implemented main part of authorization and token delivery
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.oidc.api;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record Authorization(String clientId, String userId, Instant expiration) {
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.oidc.api;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AuthorizationService {
|
||||
AuthorizationService authorize(Client client, User user, Date expiration);
|
||||
AuthorizationService addCode(Client client, User user, String code);
|
||||
AuthorizationService authorize(Client client, User user, Instant expiration);
|
||||
boolean isAuthorized(Client client, User user);
|
||||
List<User> authorizedUsers(Client client);
|
||||
List<Client> authorizedClients(User user);
|
||||
AuthorizationService revoke(Client client, User user);
|
||||
|
||||
Optional<Authorization> forCode(String code);
|
||||
}
|
||||
|
||||
@@ -2,20 +2,13 @@
|
||||
package de.srsoftware.oidc.api;
|
||||
|
||||
import static de.srsoftware.oidc.api.Constants.*;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public record Client(String id, String name, String secret, Set<String> redirectUris) {
|
||||
private static System.Logger LOG = System.getLogger(Client.class.getSimpleName());
|
||||
public Map<String, Object> map() {
|
||||
return Map.of(CLIENT_ID, id, NAME, name, SECRET, secret, REDIRECT_URIS, redirectUris);
|
||||
}
|
||||
|
||||
public String generateCode() {
|
||||
LOG.log(WARNING, "{0}.generateCode() not implemented!", getClass().getSimpleName());
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ import java.util.Optional;
|
||||
|
||||
public interface ClientService {
|
||||
Optional<Client> getClient(String clientId);
|
||||
ClientService add(Client client);
|
||||
List<Client> listClients();
|
||||
ClientService remove(Client client);
|
||||
ClientService update(Client client);
|
||||
ClientService save(Client client);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@ package de.srsoftware.oidc.api;
|
||||
|
||||
public class Constants {
|
||||
public static final String ACCESS_TOKEN = "access_token";
|
||||
public static final String ATUH_CODE = "authorization_code";
|
||||
public static final String APP_NAME = "LightOIDC";
|
||||
public static final String AUTH_CODE = "authorization_code";
|
||||
public static final String BEARER = "Bearer";
|
||||
public static final String CAUSE = "cause";
|
||||
public static final String CLIENT_ID = "client_id";
|
||||
public static final String CLIENT_SECRET = "client_secret";
|
||||
public static final String CODE = "code";
|
||||
public static final String CONFIG_PATH = "LIGHTOIDC_CONFIG_PATH";
|
||||
public static final String CONFIRMED = "confirmed";
|
||||
public static final String DEFAULT_KEY = "default_key";
|
||||
public static final String DAYS = "days";
|
||||
public static final String EXPIRES_IN = "expires_in";
|
||||
public static final String GRANT_TYPE = "grant_type";
|
||||
public static final String ID_TOKEN = "id_token";
|
||||
|
||||
@@ -16,8 +16,8 @@ dependencies {
|
||||
implementation project(':de.srsoftware.oidc.api')
|
||||
implementation project(':de.srsoftware.oidc.backend')
|
||||
implementation project(':de.srsoftware.oidc.web')
|
||||
implementation project(':de.srsoftware.oidc.datastore.file')
|
||||
}
|
||||
implementation project(':de.srsoftware.utils')
|
||||
implementation project(':de.srsoftware.oidc.datastore.file')}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
package de.srsoftware.oidc.app;
|
||||
|
||||
|
||||
import static de.srsoftware.oidc.api.Constants.*;
|
||||
import static de.srsoftware.oidc.api.Permission.MANAGE_CLIENTS;
|
||||
import static de.srsoftware.utils.Optionals.nonEmpty;
|
||||
import static de.srsoftware.utils.Paths.configDir;
|
||||
import static java.lang.System.Logger.Level.DEBUG;
|
||||
import static java.lang.System.Logger.Level.ERROR;
|
||||
import static java.lang.System.getenv;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import de.srsoftware.logging.ColorLogger;
|
||||
@@ -17,7 +21,6 @@ import de.srsoftware.oidc.datastore.file.FileStore;
|
||||
import de.srsoftware.oidc.datastore.file.UuidHasher;
|
||||
import de.srsoftware.oidc.web.Forward;
|
||||
import de.srsoftware.oidc.web.StaticPages;
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -40,19 +43,19 @@ public class Application {
|
||||
private static System.Logger LOG = new ColorLogger("Application").setLogLevel(DEBUG);
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
var argMap = map(args);
|
||||
Optional<Path> basePath = argMap.get(BASE_PATH) instanceof Path p ? Optional.of(p) : Optional.empty();
|
||||
var storageFile = new File("/tmp/lightoidc.json");
|
||||
var passwordHasher = new UuidHasher();
|
||||
var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID);
|
||||
var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID).add(MANAGE_CLIENTS);
|
||||
FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser);
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
|
||||
var argMap = map(args);
|
||||
Optional<Path> basePath = argMap.get(BASE_PATH) instanceof Path p ? Optional.of(p) : Optional.empty();
|
||||
var storageFile = (argMap.get(CONFIG_PATH) instanceof Path p ? p : configDir(APP_NAME).resolve("config.json")).toFile();
|
||||
var passwordHasher = new UuidHasher();
|
||||
var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID);
|
||||
var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID).add(MANAGE_CLIENTS);
|
||||
FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser);
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
|
||||
new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server);
|
||||
new Forward(INDEX).bindPath(ROOT).on(server);
|
||||
new WellKnownController().bindPath(WELL_KNOWN).on(server);
|
||||
new UserController(fileStore, fileStore).bindPath(API_USER).on(server);
|
||||
new TokenController(fileStore).bindPath(API_TOKEN).on(server);
|
||||
new TokenController(fileStore, fileStore, fileStore).bindPath(API_TOKEN).on(server);
|
||||
new ClientController(fileStore, fileStore, fileStore).bindPath(API_CLIENT).on(server);
|
||||
// server.setExecutor(Executors.newCachedThreadPool());
|
||||
server.setExecutor(Executors.newSingleThreadExecutor());
|
||||
@@ -62,13 +65,22 @@ public class Application {
|
||||
private static Map<String, Object> map(String[] args) {
|
||||
var tokens = new ArrayList<>(List.of(args));
|
||||
var map = new HashMap<String, Object>();
|
||||
|
||||
nonEmpty(getenv(BASE_PATH)).map(Path::of).ifPresent(path -> map.put(BASE_PATH, path));
|
||||
nonEmpty(getenv(CONFIG_PATH)).map(Path::of).ifPresent(path -> map.put(CONFIG_PATH, path));
|
||||
|
||||
// Command line arguments override environment
|
||||
while (!tokens.isEmpty()) {
|
||||
var token = tokens.remove(0);
|
||||
switch (token) {
|
||||
case "--base":
|
||||
if (tokens.isEmpty()) throw new IllegalArgumentException("--path option requires second argument!");
|
||||
if (tokens.isEmpty()) throw new IllegalArgumentException("--base option requires second argument!");
|
||||
map.put(BASE_PATH, Path.of(tokens.remove(0)));
|
||||
break;
|
||||
case "--config":
|
||||
if (tokens.isEmpty()) throw new IllegalArgumentException("--config option requires second argument!");
|
||||
map.put(CONFIG_PATH, Path.of(tokens.remove(0)));
|
||||
break;
|
||||
default:
|
||||
LOG.log(ERROR, "Unknown option: {0}", token);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@ import static java.net.HttpURLConnection.*;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.oidc.api.*;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class ClientController extends Controller {
|
||||
@@ -24,17 +27,6 @@ public class ClientController extends Controller {
|
||||
clients = clientService;
|
||||
}
|
||||
|
||||
private boolean add(HttpExchange ex, Session session) throws IOException {
|
||||
if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED");
|
||||
var json = json(ex);
|
||||
var redirects = new HashSet<String>();
|
||||
for (Object o : json.getJSONArray(REDIRECT_URIS)) {
|
||||
if (o instanceof String s) redirects.add(s);
|
||||
}
|
||||
var client = new Client(json.getString(CLIENT_ID), json.getString(NAME), json.getString(SECRET), redirects);
|
||||
clients.add(client);
|
||||
return sendContent(ex, client);
|
||||
}
|
||||
|
||||
private boolean authorize(HttpExchange ex, Session session) throws IOException {
|
||||
var user = session.user();
|
||||
@@ -48,14 +40,17 @@ public class ClientController extends Controller {
|
||||
if (!client.redirectUris().contains(redirect)) return badRequest(ex, Map.of(CAUSE, "unknown redirect uri", REDIRECT_URI, redirect));
|
||||
|
||||
if (!authorizations.isAuthorized(client, session.user())) {
|
||||
if (json.has(CONFIRMED) && json.getBoolean(CONFIRMED)) {
|
||||
authorizations.authorize(client, user, null);
|
||||
if (json.has(DAYS)) {
|
||||
var days = json.getInt(DAYS);
|
||||
var expiration = Instant.now().plus(Duration.ofDays(days));
|
||||
authorizations.authorize(client, user, expiration);
|
||||
} else {
|
||||
return sendContent(ex, Map.of(CONFIRMED, false, NAME, client.name()));
|
||||
}
|
||||
}
|
||||
var state = json.getString(STATE);
|
||||
var code = client.generateCode();
|
||||
var code = UUID.randomUUID().toString();
|
||||
authorizations.addCode(client, session.user(), code);
|
||||
return sendContent(ex, Map.of(CONFIRMED, true, CODE, code, REDIRECT_URI, redirect, STATE, state));
|
||||
}
|
||||
|
||||
@@ -94,8 +89,8 @@ public class ClientController extends Controller {
|
||||
switch (path) {
|
||||
case "/":
|
||||
return load(ex, session);
|
||||
case "/add":
|
||||
return add(ex, session);
|
||||
case "/add", "/update":
|
||||
return save(ex, session);
|
||||
case "/authorize":
|
||||
return authorize(ex, session);
|
||||
case "/list":
|
||||
@@ -123,4 +118,16 @@ public class ClientController extends Controller {
|
||||
}
|
||||
return sendEmptyResponse(HTTP_NOT_FOUND, ex);
|
||||
}
|
||||
|
||||
private boolean save(HttpExchange ex, Session session) throws IOException {
|
||||
if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED");
|
||||
var json = json(ex);
|
||||
var redirects = new HashSet<String>();
|
||||
for (Object o : json.getJSONArray(REDIRECT_URIS)) {
|
||||
if (o instanceof String s) redirects.add(s);
|
||||
}
|
||||
var client = new Client(json.getString(CLIENT_ID), json.getString(NAME), json.getString(SECRET), redirects);
|
||||
clients.save(client);
|
||||
return sendContent(ex, client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@ import static java.lang.System.Logger.Level.*;
|
||||
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.oidc.api.Client;
|
||||
import de.srsoftware.oidc.api.ClientService;
|
||||
import de.srsoftware.oidc.api.PathHandler;
|
||||
import de.srsoftware.oidc.api.*;
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -21,10 +20,14 @@ import org.jose4j.lang.JoseException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class TokenController extends PathHandler {
|
||||
private final ClientService clients;
|
||||
private final ClientService clients;
|
||||
private final AuthorizationService authorizations;
|
||||
private final UserService users;
|
||||
|
||||
public TokenController(ClientService clientService) {
|
||||
clients = clientService;
|
||||
public TokenController(AuthorizationService authorizationService, ClientService clientService, UserService userService) {
|
||||
authorizations = authorizationService;
|
||||
clients = clientService;
|
||||
users = userService;
|
||||
}
|
||||
|
||||
private Map<String, String> deserialize(String body) {
|
||||
@@ -42,27 +45,31 @@ public class TokenController extends PathHandler {
|
||||
}
|
||||
|
||||
private boolean provideToken(HttpExchange ex) throws IOException {
|
||||
var map = deserialize(body(ex));
|
||||
// TODO: check Authorization Code, → https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
||||
// TODO: check Redirect URL
|
||||
LOG.log(DEBUG, "post data: {0}", map);
|
||||
LOG.log(WARNING, "{0}.provideToken(ex) not implemented!", getClass().getSimpleName());
|
||||
var map = deserialize(body(ex));
|
||||
var grantType = map.get(GRANT_TYPE);
|
||||
if (!ATUH_CODE.equals(grantType)) sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown grant type", GRANT_TYPE, grantType));
|
||||
var optClient = Optional.ofNullable(map.get(CLIENT_ID)).flatMap(clients::getClient);
|
||||
if (optClient.isEmpty()) {
|
||||
LOG.log(ERROR, "client not found");
|
||||
return sendEmptyResponse(HTTP_BAD_REQUEST, ex);
|
||||
// TODO: send correct response
|
||||
}
|
||||
if (!AUTH_CODE.equals(grantType)) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown grant type", GRANT_TYPE, grantType));
|
||||
|
||||
var code = map.get(CODE);
|
||||
var optAuthorization = authorizations.forCode(code);
|
||||
if (optAuthorization.isEmpty()) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "invalid auth code", CODE, code));
|
||||
var authorization = optAuthorization.get();
|
||||
|
||||
var clientId = map.get(CLIENT_ID);
|
||||
if (!authorization.clientId().equals(clientId)) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "invalid client id", CLIENT_ID, clientId));
|
||||
var optClient = clients.getClient(clientId);
|
||||
if (optClient.isEmpty()) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown client", CLIENT_ID, clientId));
|
||||
var client = optClient.get();
|
||||
|
||||
var user = users.load(authorization.userId());
|
||||
if (user.isEmpty()) return sendContent(ex, 500, Map.of(ERROR, "User not found"));
|
||||
|
||||
var uri = URLDecoder.decode(map.get(REDIRECT_URI), StandardCharsets.UTF_8);
|
||||
if (!client.redirectUris().contains(uri)) sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown redirect uri", REDIRECT_URI, uri));
|
||||
|
||||
var secretFromClient = map.get(CLIENT_SECRET);
|
||||
var client = optClient.get();
|
||||
if (!client.secret().equals(secretFromClient)) {
|
||||
LOG.log(ERROR, "client secret mismatch");
|
||||
return sendEmptyResponse(HTTP_BAD_REQUEST, ex);
|
||||
// TODO: send correct response
|
||||
}
|
||||
String jwToken = createJWT(client);
|
||||
if (!client.secret().equals(secretFromClient)) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "client secret mismatch"));
|
||||
|
||||
String jwToken = createJWT(client, user.get());
|
||||
ex.getResponseHeaders().add("Cache-Control", "no-store");
|
||||
JSONObject response = new JSONObject();
|
||||
response.put(ACCESS_TOKEN, UUID.randomUUID().toString()); // TODO: wofür genau wird der verwendet, was gilt es hier zu beachten
|
||||
@@ -72,22 +79,12 @@ public class TokenController extends PathHandler {
|
||||
return sendContent(ex, response);
|
||||
}
|
||||
|
||||
private String createJWT(Client client) {
|
||||
private String createJWT(Client client, User user) {
|
||||
try {
|
||||
byte[] secretBytes = client.secret().getBytes(StandardCharsets.UTF_8);
|
||||
HmacKey hmacKey = new HmacKey(secretBytes);
|
||||
|
||||
JwtClaims claims = new JwtClaims();
|
||||
claims.setIssuer("Issuer"); // who creates the token and signs it
|
||||
claims.setAudience("Audience"); // to whom the token is intended to be sent
|
||||
claims.setExpirationTimeMinutesInTheFuture(10); // time when the token will expire (10 minutes from now)
|
||||
claims.setGeneratedJwtId(); // a unique identifier for the token
|
||||
claims.setIssuedAtToNow(); // when the token was issued/created (now)
|
||||
claims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago)
|
||||
claims.setSubject("subject"); // the subject/principal is whom the token is about
|
||||
claims.setClaim("email", "mail@example.com"); // additional claims/attributes about the subject can be added
|
||||
List<String> groups = Arrays.asList("group-one", "other-group", "group-three");
|
||||
claims.setStringListClaim("groups", groups); // multi-valued claims work too and will end up as a JSON array
|
||||
JwtClaims claims = getJwtClaims(user);
|
||||
|
||||
// A JWT is a JWS and/or a JWE with JSON claims as the payload.
|
||||
// In this example it is a JWS so we create a JsonWebSignature object.
|
||||
@@ -105,4 +102,16 @@ public class TokenController extends PathHandler {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static JwtClaims getJwtClaims(User user) {
|
||||
JwtClaims claims = new JwtClaims();
|
||||
claims.setIssuer(APP_NAME); // who creates the token and signs it
|
||||
claims.setExpirationTimeMinutesInTheFuture(10); // time when the token will expire (10 minutes from now)
|
||||
claims.setGeneratedJwtId(); // a unique identifier for the token
|
||||
claims.setIssuedAtToNow(); // when the token was issued/created (now)
|
||||
claims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago)
|
||||
claims.setSubject(user.uuid()); // the subject/principal is whom the token is about
|
||||
claims.setClaim("email", user.email()); // additional claims/attributes about the subject can be added
|
||||
return claims;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,16 @@ import java.util.*;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class FileStore implements AuthorizationService, ClientService, SessionService, UserService {
|
||||
private static final String CLIENTS = "clients";
|
||||
private static final String EXPIRATION = "expiration";
|
||||
private static final String NAME = "name";
|
||||
private static final String REDIRECT_URIS = "redirect_uris";
|
||||
private static final String SECRET = "secret";
|
||||
private static final String SESSIONS = "sessions";
|
||||
private static final String USERS = "users";
|
||||
private static final String USER = "user";
|
||||
private static final String AUTHORIZATIONS = "authorizations";
|
||||
private static final String CLIENTS = "clients";
|
||||
private static final String CODES = "codes";
|
||||
private static final String EXPIRATION = "expiration";
|
||||
private static final String NAME = "name";
|
||||
private static final String REDIRECT_URIS = "redirect_uris";
|
||||
private static final String SECRET = "secret";
|
||||
private static final String SESSIONS = "sessions";
|
||||
private static final String USERS = "users";
|
||||
private static final String USER = "user";
|
||||
|
||||
private final Path storageFile;
|
||||
private final JSONObject json;
|
||||
@@ -61,6 +63,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
||||
|
||||
@Override
|
||||
public FileStore init(User defaultUser) {
|
||||
if (!json.has(AUTHORIZATIONS)) json.put(AUTHORIZATIONS, new JSONObject());
|
||||
if (!json.has(CLIENTS)) json.put(CLIENTS, new JSONObject());
|
||||
if (!json.has(SESSIONS)) json.put(SESSIONS, new JSONObject());
|
||||
if (!json.has(USERS)) save(defaultUser);
|
||||
@@ -179,6 +182,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
||||
if (expiration.isAfter(Instant.now())) {
|
||||
return load(userId).map(user -> new Session(user, expiration, sessionId));
|
||||
}
|
||||
dropSession(sessionId);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return Optional.empty();
|
||||
@@ -197,13 +201,6 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
||||
|
||||
/** client service methods **/
|
||||
|
||||
@Override
|
||||
public ClientService add(Client client) {
|
||||
json.getJSONObject(CLIENTS).put(client.id(), Map.of(NAME, client.name(), SECRET, client.secret(), REDIRECT_URIS, client.redirectUris()));
|
||||
save();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Client> getClient(String clientId) {
|
||||
var clients = json.getJSONObject(CLIENTS);
|
||||
@@ -211,13 +208,6 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private Client toClient(String clientId, JSONObject clientData) {
|
||||
var redirectUris = new HashSet<String>();
|
||||
for (var o : clientData.getJSONArray(REDIRECT_URIS)) {
|
||||
if (o instanceof String s) redirectUris.add(s);
|
||||
}
|
||||
return new Client(clientId, clientData.getString(NAME), clientData.getString(SECRET), redirectUris);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Client> listClients() {
|
||||
@@ -235,19 +225,75 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientService update(Client client) {
|
||||
return null;
|
||||
public ClientService save(Client client) {
|
||||
json.getJSONObject(CLIENTS).put(client.id(), Map.of(NAME, client.name(), SECRET, client.secret(), REDIRECT_URIS, client.redirectUris()));
|
||||
save();
|
||||
return this;
|
||||
}
|
||||
|
||||
private Client toClient(String clientId, JSONObject clientData) {
|
||||
var redirectUris = new HashSet<String>();
|
||||
for (var o : clientData.getJSONArray(REDIRECT_URIS)) {
|
||||
if (o instanceof String s) redirectUris.add(s);
|
||||
}
|
||||
return new Client(clientId, clientData.getString(NAME), clientData.getString(SECRET), redirectUris);
|
||||
}
|
||||
|
||||
|
||||
/*** Authorization service methods ***/
|
||||
|
||||
@Override
|
||||
public AuthorizationService authorize(Client client, User user, Date expiration) {
|
||||
return null;
|
||||
public Optional<Authorization> forCode(String code) {
|
||||
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
||||
if (!authorizations.has(code)) return Optional.empty();
|
||||
String authId = authorizations.getString(code);
|
||||
if (!authorizations.has(authId)) {
|
||||
authorizations.remove(code);
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
var expiration = Instant.ofEpochSecond(authorizations.getLong(authId));
|
||||
if (expiration.isAfter(Instant.now())) {
|
||||
String[] parts = authId.split("@");
|
||||
return Optional.of(new Authorization(parts[1], parts[0], expiration));
|
||||
}
|
||||
authorizations.remove(authId);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationService addCode(Client client, User user, String code) {
|
||||
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
||||
authorizations.put(code, authorizationId(user, client));
|
||||
save();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationService authorize(Client client, User user, Instant expiration) {
|
||||
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
||||
authorizations.put(authorizationId(user, client), expiration.getEpochSecond());
|
||||
return this;
|
||||
}
|
||||
|
||||
private String authorizationId(User user, Client client) {
|
||||
return String.join("@", user.uuid(), client.id());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthorized(Client client, User user) {
|
||||
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
||||
var authId = authorizationId(user, client);
|
||||
if (!authorizations.has(authId)) return false;
|
||||
|
||||
try {
|
||||
if (Instant.ofEpochSecond(authorizations.getLong(authId)).isAfter(Instant.now())) return true;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
revoke(client, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -263,6 +309,10 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
||||
|
||||
@Override
|
||||
public AuthorizationService revoke(Client client, User user) {
|
||||
return null;
|
||||
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
||||
var authId = authorizationId(user, client);
|
||||
if (!authorizations.has(authId)) return this;
|
||||
authorizations.remove(authId);
|
||||
return save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
<div id="content" style="display: none">
|
||||
<h1>Authorization</h1>
|
||||
Confirmation required: are you shure you want to grant access to <span id="name">some client</span>?
|
||||
<button type="button" onclick="grantAutorization()">Yes</button>
|
||||
<button type="button" onclick="grantAutorization(1)">Yes - 1 day</button>
|
||||
<button type="button" onclick="grantAutorization(7)">Yes - 1 week</button>
|
||||
<button type="button" onclick="grantAutorization(30)">Yes - 1 month</button>
|
||||
<button type="button" onclick="grantAutorization(365)">Yes - 1 year</button>
|
||||
<button type="button" onclick="denyAutorization()">No</button>
|
||||
</div>
|
||||
<div id="error" class="error" style="display: none"></div>
|
||||
|
||||
@@ -24,8 +24,8 @@ async function handleResponse(response){
|
||||
}
|
||||
}
|
||||
|
||||
function grantAutorization(){
|
||||
json.confirmed = true;
|
||||
function grantAutorization(days){
|
||||
json.days = days;
|
||||
backendAutorization();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,25 +15,25 @@
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<td><input type="text" disabled="true" id="client_id" /></td>
|
||||
<td><input type="text" disabled="true" id="client-id" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td><input type="text" id="name" /></td>
|
||||
<td><input type="text" id="client-name" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Secret</th>
|
||||
<td><input type="text" id="secret" /></td>
|
||||
<td><input type="text" id="client-secret" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Redirect URIs</th>
|
||||
<td>
|
||||
<textarea id="redirects"></textarea>
|
||||
<textarea id="redirect-urls"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><button type="button" id="button">Update</button></td>
|
||||
<td><button type="button" id="button" onclick="updateClient();">Update</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
@@ -1,20 +1,52 @@
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var id = params.get('id');
|
||||
|
||||
|
||||
async function handleLoadResponse(response){
|
||||
if (response.ok){
|
||||
var json = await response.json();
|
||||
get('client-id').value = json.client_id;
|
||||
get('client-name').value = json.name;
|
||||
get('client-secret').value = json.secret;
|
||||
get('redirect-urls').value = json.redirect_uris.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateResponse(response){
|
||||
if (response.ok) {
|
||||
enable('button');
|
||||
setText('button','saved.');
|
||||
}
|
||||
}
|
||||
|
||||
function resetButton(){
|
||||
enable('button');
|
||||
setText('button','Update')
|
||||
}
|
||||
|
||||
function updateClient(){
|
||||
disable('button');
|
||||
setText('button','sent data…')
|
||||
var data = {
|
||||
client_id : getValue('client-id'),
|
||||
name : getValue('client-name'),
|
||||
secret : getValue('client-secret'),
|
||||
redirect_uris : getValue('redirect-urls').split("\n")
|
||||
};
|
||||
fetch(client_controller+'/update',{
|
||||
method : 'POST',
|
||||
headers : {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body : JSON.stringify(data)
|
||||
}).then(handleUpdateResponse);
|
||||
setTimeout(resetButton,4000);
|
||||
}
|
||||
|
||||
fetch(api+'/client',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
client_id : id
|
||||
})
|
||||
}).then(handleResponse);
|
||||
|
||||
async function handleResponse(response){
|
||||
if (response.ok){
|
||||
var json = await response.json();
|
||||
get('client_id').value = json.client_id;
|
||||
get('name').value = json.name;
|
||||
get('secret').value = json.secret;
|
||||
get('redirects').value = json.redirect_uris.join("\n");
|
||||
}
|
||||
}
|
||||
}).then(handleLoadResponse);
|
||||
@@ -4,7 +4,7 @@
|
||||
<title>Light OIDC</title>
|
||||
<script src="common.js"></script>
|
||||
<script src="user.js"></script>
|
||||
<script src="newclient.js"></script>
|
||||
<script src="new_client.js"></script>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
19
de.srsoftware.utils/build.gradle
Normal file
19
de.srsoftware.utils/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
group = 'de.srsoftware'
|
||||
version = '1.0-SNAPSHOT'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation platform('org.junit:junit-bom:5.10.0')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.utils;
|
||||
import java.util.Optional;
|
||||
|
||||
public class Optionals {
|
||||
public static <T> Optional<T> optional(T val) {
|
||||
return Optional.ofNullable(val);
|
||||
}
|
||||
|
||||
public static Optional<String> nonEmpty(String text) {
|
||||
return text == null || text.isBlank() ? Optional.empty() : optional(text.trim());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.utils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class Paths {
|
||||
public static Path configDir(String applicationName) {
|
||||
String home = System.getProperty("user.home");
|
||||
return Path.of(home, ".config", applicationName);
|
||||
}
|
||||
|
||||
public static Path configDir(Class clazz) {
|
||||
return configDir(clazz.getSimpleName());
|
||||
}
|
||||
|
||||
public static Path configDir(Object clazz) {
|
||||
return configDir(clazz.getClass());
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,5 @@ include 'de.srsoftware.oidc.backend'
|
||||
include 'de.srsoftware.oidc.datastore.file'
|
||||
include 'de.srsoftware.cookies'
|
||||
include 'de.srsoftware.logging'
|
||||
include 'de.srsoftware.utils'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user