From 8bfaf22084c2580b097bb87df08ead1df6454293 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Mon, 16 Sep 2024 23:56:29 +0200 Subject: [PATCH] tied nonce to AuthorizationService by dedicated methods Signed-off-by: Stephan Richter --- .../oidc/api/AuthorizationService.java | 6 ++- .../oidc/api/data/Authorization.java | 2 +- .../srsoftware/oidc/api/AuthServiceTest.java | 12 ++--- .../oidc/backend/ClientController.java | 5 +- .../oidc/backend/TokenController.java | 13 +++-- .../oidc/datastore/file/FileStore.java | 49 ++++++++++--------- .../datastore/sqlite/SqliteAuthService.java | 31 ++++++++---- 7 files changed, 71 insertions(+), 47 deletions(-) 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 d0b33bc..a783c98 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 @@ -8,7 +8,11 @@ import java.util.Collection; import java.util.Optional; public interface AuthorizationService { - AuthorizationService authorize(String userId, String clientId, Collection scopes, String nonce, Instant expiration); + AuthorizationService authorize(String userId, String clientId, Collection scopes, Instant expiration); Optional consumeAuthorization(String authCode); AuthResult getAuthorization(String userId, String clientId, Collection scopes); + + Optional consumeNonce(String uuid, String id); + + void nonce(String uuid, String id, String string); } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Authorization.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Authorization.java index cbc02a7..c935976 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Authorization.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Authorization.java @@ -1,5 +1,5 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.api.data; -public record Authorization(String clientId, String userId, AuthorizedScopes scopes, String nonce) { +public record Authorization(String clientId, String userId, AuthorizedScopes scopes) { } \ No newline at end of file diff --git a/de.srsoftware.oidc.api/src/test/java/de/srsoftware/oidc/api/AuthServiceTest.java b/de.srsoftware.oidc.api/src/test/java/de/srsoftware/oidc/api/AuthServiceTest.java index cd9cf7a..27cf08f 100644 --- a/de.srsoftware.oidc.api/src/test/java/de/srsoftware/oidc/api/AuthServiceTest.java +++ b/de.srsoftware.oidc.api/src/test/java/de/srsoftware/oidc/api/AuthServiceTest.java @@ -26,10 +26,9 @@ public abstract class AuthServiceTest { var authorizationService = authorizationService(); var userId1 = uuid(); var expiration = Instant.now(); - var nonce = uuid(); - authorizationService.authorize(userId1, CLIENT1, SCOPES1, nonce, expiration); - expiration = Instant.now().plusSeconds(3600).truncatedTo(SECONDS); // test overwrite - authorizationService.authorize(userId1, CLIENT1, SCOPES1, nonce, expiration); // test overwrite + authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration); + expiration = Instant.now().plusSeconds(3600).truncatedTo(SECONDS); // test overwrite + authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration); // test overwrite var authorization = authorizationService.getAuthorization(userId1, CLIENT1, Set.of(OPENID)); assertEquals(1, authorization.authorizedScopes().scopes().size()); assertTrue(authorization.authorizedScopes().scopes().contains(OPENID)); @@ -53,10 +52,9 @@ public abstract class AuthServiceTest { public void testConsume() { var authorizationService = authorizationService(); - var nonce = uuid(); var userId1 = uuid(); var expiration = Instant.now().plusSeconds(3600).truncatedTo(SECONDS); - authorizationService.authorize(userId1, CLIENT1, SCOPES1, nonce, expiration); + authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration); var authResult = authorizationService.getAuthorization(userId1, CLIENT1, Set.of(OPENID)); var authCode = authResult.authCode(); assertNotNull(authCode); @@ -75,5 +73,5 @@ public abstract class AuthServiceTest { assertTrue(optAuth.isEmpty()); } - // TODO: test nonce passing + // TODO: test nonce methods } 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 d1c524e..c5ccfc0 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 @@ -79,9 +79,10 @@ public class ClientController extends Controller { var days = authorized.getInt("days"); var list = new ArrayList(); authorized.getJSONArray("scopes").forEach(scope -> list.add(scope.toString())); - var nonce = json.has(NONCE) ? json.getString(NONCE) : null; - authorizations.authorize(user.uuid(), client.id(), list, nonce, Instant.now().plus(days, ChronoUnit.DAYS)); + authorizations.authorize(user.uuid(), client.id(), list, Instant.now().plus(days, ChronoUnit.DAYS)); } + if (json.has(NONCE)) authorizations.nonce(user.uuid(), client.id(), json.getString(NONCE)); + var authResult = authorizations.getAuthorization(user.uuid(), client.id(), scopes); if (!authResult.unauthorizedScopes().isEmpty()) { 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 270cbdc..73c5305 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 @@ -115,7 +115,7 @@ public class TokenController extends PathHandler { var user = optUser.get(); var accessToken = users.accessToken(user); - String jwToken = createJWT(client, user, accessToken, authorization.nonce()); + String jwToken = createJWT(client, user, accessToken); ex.getResponseHeaders().add("Cache-Control", "no-store"); JSONObject response = new JSONObject(); response.put(ACCESS_TOKEN, accessToken.id()); @@ -126,13 +126,13 @@ public class TokenController extends PathHandler { return sendContent(ex, response); } - private String createJWT(Client client, User user, AccessToken accessToken, String nonce) { + private String createJWT(Client client, User user, AccessToken accessToken) { try { PublicJsonWebKey key = keyManager.getKey(); var algo = key.getAlgorithm(); var atHash = this.atHash(algo, accessToken); key.setUse("sig"); - JwtClaims claims = createIdTokenClaims(user, client, atHash, nonce); + JwtClaims claims = createIdTokenClaims(user, client, atHash); // 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. @@ -167,7 +167,9 @@ public class TokenController extends PathHandler { } } - private JwtClaims createIdTokenClaims(User user, Client client, String atHash, String nonce) { + private JwtClaims createIdTokenClaims(User user, Client client, String atHash) { + var optNonce = authorizations.consumeNonce(user.uuid(), client.id()); + optNonce.ifPresent(nonce -> LOG.log(System.Logger.Level.ERROR, "consumed nonce: %s", nonce)); JwtClaims claims = new JwtClaims(); // required claims: @@ -179,7 +181,8 @@ public class TokenController extends PathHandler { claims.setClaim(AT_HASH, atHash); claims.setClaim(CLIENT_ID, client.id()); claims.setClaim(EMAIL, user.email()); // additional claims/attributes about the subject can be added - if (nonce != null) claims.setClaim(NONCE, nonce); + + optNonce.ifPresent(nonce -> claims.setClaim(NONCE, nonce)); claims.setGeneratedJwtId(); // a unique identifier for the token 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 39f4cf5..d4c203a 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 @@ -37,6 +37,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe private final PasswordHasher passwordHasher; private Map accessTokens = new HashMap<>(); private Map authCodes = new HashMap<>(); + private Map nonceMap = new HashMap<>(); private Authenticator auth; public FileStore(File storage, PasswordHasher passwordHasher) throws IOException { @@ -72,16 +73,15 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe var clients = authorizations.getJSONObject(userId); var clientIds = Set.copyOf(clients.keySet()); for (var clientId : clientIds) { - var clientData = clients.getJSONObject(clientId); - var scopeMap = clientData.getJSONObject(SCOPE); - var scopes = Set.copyOf(scopeMap.keySet()); + var client = clients.getJSONObject(clientId); + var scopes = Set.copyOf(client.keySet()); for (var scope : scopes) { - var expiration = Instant.ofEpochSecond(scopeMap.getLong(scope)); + var expiration = Instant.ofEpochSecond(client.getLong(scope)); if (expiration.isBefore(now)) { - scopeMap.remove(scope); + client.remove(scope); } } - if (scopeMap.isEmpty()) clients.remove(clientId); + if (client.isEmpty()) clients.remove(clientId); } if (clients.isEmpty()) authorizations.remove(userId); } @@ -298,7 +298,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe @Override public ClientService save(Client client) { if (!json.has(CLIENTS)) json.put(CLIENTS, new JSONObject()); - json.getJSONObject(CLIENTS).put(client.id(), client.map()); + json.getJSONObject(CLIENTS).put(client.id(), Map.of(NAME, client.name(), SECRET, client.secret(), REDIRECT_URIS, client.redirectUris())); save(); return this; } @@ -311,7 +311,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe return new Client(clientId, clientData.getString(NAME), clientData.getString(SECRET), redirectUris); } - /*** ClaimAuthorizationService methods ***/ + /*** AuthorizationService methods ***/ private String authCode(Authorization authorization) { var code = uuid(); authCodes.put(code, authorization); @@ -319,20 +319,13 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } @Override - public AuthorizationService authorize(String userId, String clientId, Collection scopes, String nonce, Instant expiration) { + public AuthorizationService authorize(String userId, String clientId, Collection scopes, Instant expiration) { if (!json.has(AUTHORIZATIONS)) json.put(AUTHORIZATIONS, new JSONObject()); var authorizations = json.getJSONObject(AUTHORIZATIONS); if (!authorizations.has(userId)) authorizations.put(userId, new JSONObject()); var userAuthorizations = authorizations.getJSONObject(userId); if (!userAuthorizations.has(clientId)) userAuthorizations.put(clientId, new JSONObject()); - var clientData = userAuthorizations.getJSONObject(clientId); - if (nonce != null) { - clientData.put(NONCE, nonce); - } else { - if (clientData.has(NONCE)) clientData.remove(NONCE); - } - if (!clientData.has(SCOPE)) clientData.put(SCOPE, new JSONObject()); - var clientScopes = clientData.getJSONObject(SCOPE); + var clientScopes = userAuthorizations.getJSONObject(clientId); for (var scope : scopes) clientScopes.put(scope, expiration.getEpochSecond()); save(); return this; @@ -344,15 +337,19 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe return nullable(authCodes.remove(authCode)); } + @Override + public Optional consumeNonce(String userId, String clientId) { + var nonceKey = String.join("@", userId, clientId); + return nullable(nonceMap.get(nonceKey)); + } + @Override public AuthResult getAuthorization(String userId, String clientId, Collection scopes) { if (!json.has(AUTHORIZATIONS)) return unauthorized(scopes); var authorizations = json.getJSONObject(AUTHORIZATIONS); var userAuthorizations = authorizations.has(userId) ? authorizations.getJSONObject(userId) : null; if (userAuthorizations == null) return unauthorized(scopes); - var clientData = userAuthorizations.has(clientId) ? userAuthorizations.getJSONObject(clientId) : null; - if (clientData == null) return unauthorized(scopes); - var clientScopes = clientData.has(SCOPE) ? clientData.getJSONObject(SCOPE) : null; + var clientScopes = userAuthorizations.has(clientId) ? userAuthorizations.getJSONObject(clientId) : null; if (clientScopes == null) return unauthorized(scopes); var now = Instant.now(); var authorizedScopes = new HashSet(); @@ -371,11 +368,19 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } if (authorizedScopes.isEmpty()) return unauthorized(scopes); - String nonce = clientData.has(NONCE) ? clientData.getString(NONCE) : null; - var authorization = new Authorization(clientId, userId, new AuthorizedScopes(authorizedScopes, earliestExpiration), nonce); + var authorization = new Authorization(clientId, userId, new AuthorizedScopes(authorizedScopes, earliestExpiration)); return new AuthResult(authorization.scopes(), unauthorizedScopes, authCode(authorization)); } + @Override + public void nonce(String userId, String clientId, String nonce) { + var nonceKey = String.join("@", userId, clientId); + if (nonce != null) { + nonceMap.put(nonceKey, nonce); + } else + nonceMap.remove(nonceKey); + } + private AuthResult unauthorized(Collection scopes) { return new AuthResult(null, new HashSet<>(scopes), null); } diff --git a/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthService.java b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthService.java index 5b2c749..3702807 100644 --- a/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthService.java +++ b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthService.java @@ -21,11 +21,12 @@ public class SqliteAuthService extends SqliteStore implements AuthorizationServi private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'"; private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'"; - private static final String CREATE_AUTHSTORE_TABLE = "CREATE TABLE IF NOT EXISTS authorizations(userId VARCHAR(255), clientId VARCHAR(255), scope VARCHAR(255), expiration LONG, nonce VARCHAR(255), PRIMARY KEY(userId, clientId, scope));"; - private static final String SAVE_AUTHORIZATION = "INSERT INTO authorizations(userId, clientId, scope, nonce, expiration) VALUES (?,?,?,?,?) ON CONFLICT DO UPDATE SET nonce = ?, expiration = ?"; + private static final String CREATE_AUTHSTORE_TABLE = "CREATE TABLE IF NOT EXISTS authorizations(userId VARCHAR(255), clientId VARCHAR(255), scope VARCHAR(255), expiration LONG, PRIMARY KEY(userId, clientId, scope));"; + private static final String SAVE_AUTHORIZATION = "INSERT INTO authorizations(userId, clientId, scope, expiration) VALUES (?,?,?,?) ON CONFLICT DO UPDATE SET expiration = ?"; private static final String SELECT_AUTH = "SELECT * FROM authorizations WHERE userid = ? AND clientId = ? AND scope IN"; private Map authCodes = new HashMap<>(); + private Map nonceMap = new HashMap<>(); public SqliteAuthService(Connection connection) throws SQLException { super(connection); @@ -76,16 +77,14 @@ public class SqliteAuthService extends SqliteStore implements AuthorizationServi } @Override - public AuthorizationService authorize(String userId, String clientId, Collection scopes, String nonce, Instant expiration) { + public AuthorizationService authorize(String userId, String clientId, Collection scopes, Instant expiration) { try { conn.setAutoCommit(false); var stmt = conn.prepareStatement(SAVE_AUTHORIZATION); stmt.setString(1, userId); stmt.setString(2, clientId); - stmt.setString(4, nonce); + stmt.setLong(4, expiration.getEpochSecond()); stmt.setLong(5, expiration.getEpochSecond()); - stmt.setString(6, nonce); - stmt.setLong(7, expiration.getEpochSecond()); for (var scope : scopes) { stmt.setString(3, scope); stmt.execute(); @@ -103,6 +102,12 @@ public class SqliteAuthService extends SqliteStore implements AuthorizationServi return nullable(authCodes.remove(authCode)); } + @Override + public Optional consumeNonce(String userId, String clientId) { + var nonceKey = String.join("@", userId, clientId); + return nullable(nonceMap.get(nonceKey)); + } + @Override public AuthResult getAuthorization(String userId, String clientId, Collection scopes) { try { @@ -130,12 +135,20 @@ public class SqliteAuthService extends SqliteStore implements AuthorizationServi } rs.close(); if (authorized.isEmpty()) return new AuthResult(null, unauthorized, null); - var authorizedScopes = new AuthorizedScopes(authorized, earliestExp); - String nonce = null; - var authorization = new Authorization(clientId, userId, authorizedScopes, nonce); + var authorizedScopes = new AuthorizedScopes(authorized, earliestExp); + var authorization = new Authorization(clientId, userId, authorizedScopes); return new AuthResult(authorizedScopes, unauthorized, authCode(authorization)); } catch (SQLException e) { throw new RuntimeException(e); } } + + @Override + public void nonce(String userId, String clientId, String nonce) { + var nonceKey = String.join("@", userId, clientId); + if (nonce != null) { + nonceMap.put(nonceKey, nonce); + } else + nonceMap.remove(nonceKey); + } }