From 1e8ca6dc3a73151cb9ebd0f2bd9d200886346648 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 30 Jul 2024 00:22:21 +0200 Subject: [PATCH] implemented main part of authorization and token delivery Signed-off-by: Stephan Richter --- .../de/srsoftware/oidc/api/Authorization.java | 7 ++ .../oidc/api/AuthorizationService.java | 8 +- .../java/de/srsoftware/oidc/api/Client.java | 7 -- .../de/srsoftware/oidc/api/ClientService.java | 3 +- .../de/srsoftware/oidc/api/Constants.java | 6 +- de.srsoftware.oidc.app/build.gradle | 4 +- .../de/srsoftware/oidc/app/Application.java | 34 ++++-- .../oidc/backend/ClientController.java | 39 ++++--- .../oidc/backend/TokenController.java | 83 +++++++------- .../oidc/datastore/file/FileStore.java | 104 +++++++++++++----- .../src/main/resources/en/authorization.html | 5 +- .../src/main/resources/en/authorization.js | 4 +- .../src/main/resources/en/edit_client.html | 10 +- .../src/main/resources/en/edit_client.js | 54 +++++++-- .../en/{newclient.html => new_client.html} | 2 +- .../en/{newclient.js => new_client.js} | 0 de.srsoftware.utils/build.gradle | 19 ++++ .../java/de/srsoftware/utils/Optionals.java | 13 +++ .../main/java/de/srsoftware/utils/Paths.java | 19 ++++ settings.gradle | 1 + 20 files changed, 296 insertions(+), 126 deletions(-) create mode 100644 de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Authorization.java rename de.srsoftware.oidc.web/src/main/resources/en/{newclient.html => new_client.html} (95%) rename de.srsoftware.oidc.web/src/main/resources/en/{newclient.js => new_client.js} (100%) create mode 100644 de.srsoftware.utils/build.gradle create mode 100644 de.srsoftware.utils/src/main/java/de/srsoftware/utils/Optionals.java create mode 100644 de.srsoftware.utils/src/main/java/de/srsoftware/utils/Paths.java diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Authorization.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Authorization.java new file mode 100644 index 0000000..d766c20 --- /dev/null +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Authorization.java @@ -0,0 +1,7 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.api; + +import java.time.Instant; + +public record Authorization(String clientId, String userId, Instant expiration) { +} diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizationService.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizationService.java index 88cdfe4..c9e4022 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizationService.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizationService.java @@ -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 authorizedUsers(Client client); List authorizedClients(User user); AuthorizationService revoke(Client client, User user); + + Optional forCode(String code); } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Client.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Client.java index c786ff3..502adf3 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Client.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Client.java @@ -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 redirectUris) { private static System.Logger LOG = System.getLogger(Client.class.getSimpleName()); public Map 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(); - } } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/ClientService.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/ClientService.java index 1ec300e..5216f7e 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/ClientService.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/ClientService.java @@ -6,8 +6,7 @@ import java.util.Optional; public interface ClientService { Optional getClient(String clientId); - ClientService add(Client client); List listClients(); ClientService remove(Client client); - ClientService update(Client client); + ClientService save(Client client); } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java index a2c03e4..6c20c55 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java @@ -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"; diff --git a/de.srsoftware.oidc.app/build.gradle b/de.srsoftware.oidc.app/build.gradle index 4eeb19c..b5da0a5 100644 --- a/de.srsoftware.oidc.app/build.gradle +++ b/de.srsoftware.oidc.app/build.gradle @@ -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() diff --git a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java index 144b93c..29004ce 100644 --- a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java +++ b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java @@ -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 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 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 map(String[] args) { var tokens = new ArrayList<>(List.of(args)); var map = new HashMap(); + + 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); } diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java index 6fb292a..1785475 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java @@ -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(); - 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(); + 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); + } } diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java index 265bb87..e5549f8 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java @@ -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 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 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; + } } diff --git a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java index 28bc07d..0cbab2f 100644 --- a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java +++ b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java @@ -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 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(); - 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 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(); + 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 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(); } } diff --git a/de.srsoftware.oidc.web/src/main/resources/en/authorization.html b/de.srsoftware.oidc.web/src/main/resources/en/authorization.html index f3d0077..109988a 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/authorization.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/authorization.html @@ -12,7 +12,10 @@ diff --git a/de.srsoftware.oidc.web/src/main/resources/en/authorization.js b/de.srsoftware.oidc.web/src/main/resources/en/authorization.js index ce29ea0..920c40e 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/authorization.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/authorization.js @@ -24,8 +24,8 @@ async function handleResponse(response){ } } -function grantAutorization(){ - json.confirmed = true; +function grantAutorization(days){ + json.days = days; backendAutorization(); } diff --git a/de.srsoftware.oidc.web/src/main/resources/en/edit_client.html b/de.srsoftware.oidc.web/src/main/resources/en/edit_client.html index 1871ba0..0c63311 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/edit_client.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/edit_client.html @@ -15,25 +15,25 @@ - + - + - + - +
ID
Name
Secret
Redirect URIs - +
diff --git a/de.srsoftware.oidc.web/src/main/resources/en/edit_client.js b/de.srsoftware.oidc.web/src/main/resources/en/edit_client.js index 196e77d..3e55089 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/edit_client.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/edit_client.js @@ -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); \ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/newclient.html b/de.srsoftware.oidc.web/src/main/resources/en/new_client.html similarity index 95% rename from de.srsoftware.oidc.web/src/main/resources/en/newclient.html rename to de.srsoftware.oidc.web/src/main/resources/en/new_client.html index 9e31e8d..a0290b5 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/newclient.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/new_client.html @@ -4,7 +4,7 @@ Light OIDC - + diff --git a/de.srsoftware.oidc.web/src/main/resources/en/newclient.js b/de.srsoftware.oidc.web/src/main/resources/en/new_client.js similarity index 100% rename from de.srsoftware.oidc.web/src/main/resources/en/newclient.js rename to de.srsoftware.oidc.web/src/main/resources/en/new_client.js diff --git a/de.srsoftware.utils/build.gradle b/de.srsoftware.utils/build.gradle new file mode 100644 index 0000000..a55b584 --- /dev/null +++ b/de.srsoftware.utils/build.gradle @@ -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() +} \ No newline at end of file diff --git a/de.srsoftware.utils/src/main/java/de/srsoftware/utils/Optionals.java b/de.srsoftware.utils/src/main/java/de/srsoftware/utils/Optionals.java new file mode 100644 index 0000000..255366d --- /dev/null +++ b/de.srsoftware.utils/src/main/java/de/srsoftware/utils/Optionals.java @@ -0,0 +1,13 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.utils; +import java.util.Optional; + +public class Optionals { + public static Optional optional(T val) { + return Optional.ofNullable(val); + } + + public static Optional nonEmpty(String text) { + return text == null || text.isBlank() ? Optional.empty() : optional(text.trim()); + } +} diff --git a/de.srsoftware.utils/src/main/java/de/srsoftware/utils/Paths.java b/de.srsoftware.utils/src/main/java/de/srsoftware/utils/Paths.java new file mode 100644 index 0000000..19940c1 --- /dev/null +++ b/de.srsoftware.utils/src/main/java/de/srsoftware/utils/Paths.java @@ -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()); + } +} diff --git a/settings.gradle b/settings.gradle index 2ceaf4b..8f326cf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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'