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 126cf51..d0b33bc 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,7 @@ import java.util.Collection; import java.util.Optional; public interface AuthorizationService { - AuthorizationService authorize(String userId, String clientId, Collection scopes, Instant expiration); + AuthorizationService authorize(String userId, String clientId, Collection scopes, String nonce, Instant expiration); Optional consumeAuthorization(String authCode); AuthResult getAuthorization(String userId, String clientId, Collection scopes); } 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 c935976..cbc02a7 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) { +public record Authorization(String clientId, String userId, AuthorizedScopes scopes, String nonce) { } \ No newline at end of file diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Client.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Client.java index 4d36b25..ac9ef24 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Client.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Client.java @@ -3,14 +3,12 @@ package de.srsoftware.oidc.api.data; import static de.srsoftware.oidc.api.Constants.*; -import static de.srsoftware.utils.Optionals.nullable; import java.util.*; public final class Client { private static System.Logger LOG = System.getLogger(Client.class.getSimpleName()); private final String id, name, secret; - private String nonce = null; private final Set redirectUris; public Client(String id, String name, String secret, Set redirectUris) { @@ -33,16 +31,6 @@ public final class Client { return name; } - public Client nonce(String newVal) { - nonce = newVal; - ; - return this; - } - - public Optional nonce() { - return nullable(nonce); - } - public String secret() { return secret; } 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 9be301a..cd9cf7a 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,9 +26,10 @@ public abstract class AuthServiceTest { var authorizationService = authorizationService(); var userId1 = uuid(); var expiration = Instant.now(); - authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration); - expiration = Instant.now().plusSeconds(3600).truncatedTo(SECONDS); // test overwrite - authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration); // test overwrite + 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 var authorization = authorizationService.getAuthorization(userId1, CLIENT1, Set.of(OPENID)); assertEquals(1, authorization.authorizedScopes().scopes().size()); assertTrue(authorization.authorizedScopes().scopes().contains(OPENID)); @@ -52,9 +53,10 @@ 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, expiration); + authorizationService.authorize(userId1, CLIENT1, SCOPES1, nonce, expiration); var authResult = authorizationService.getAuthorization(userId1, CLIENT1, Set.of(OPENID)); var authCode = authResult.authCode(); assertNotNull(authCode); @@ -72,4 +74,6 @@ public abstract class AuthServiceTest { optAuth = authorizationService.consumeAuthorization(authCode); assertTrue(optAuth.isEmpty()); } + + // TODO: test nonce passing } 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 54f7427..d1c524e 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 @@ -73,13 +73,14 @@ public class ClientController extends Controller { var redirect = json.getString(REDIRECT_URI); if (!client.redirectUris().contains(redirect)) authorizationError(ex, INVALID_REDIRECT_URI, "unknown redirect uri: %s".formatted(redirect), state); - client.nonce(json.has(NONCE) ? json.getString(NONCE) : null); + if (json.has(AUTHORZED)) { // user did consent var authorized = json.getJSONObject(AUTHORZED); var days = authorized.getInt("days"); var list = new ArrayList(); authorized.getJSONArray("scopes").forEach(scope -> list.add(scope.toString())); - authorizations.authorize(user.uuid(), client.id(), list, Instant.now().plus(days, ChronoUnit.DAYS)); + var nonce = json.has(NONCE) ? json.getString(NONCE) : null; + authorizations.authorize(user.uuid(), client.id(), list, nonce, Instant.now().plus(days, ChronoUnit.DAYS)); } var authResult = authorizations.getAuthorization(user.uuid(), client.id(), scopes); 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 d1c6af9..270cbdc 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); + String jwToken = createJWT(client, user, accessToken, authorization.nonce()); 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) { + private String createJWT(Client client, User user, AccessToken accessToken, String nonce) { try { PublicJsonWebKey key = keyManager.getKey(); var algo = key.getAlgorithm(); var atHash = this.atHash(algo, accessToken); key.setUse("sig"); - JwtClaims claims = createIdTokenClaims(user, client, atHash); + JwtClaims claims = createIdTokenClaims(user, client, atHash, nonce); // 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,7 @@ public class TokenController extends PathHandler { } } - private JwtClaims createIdTokenClaims(User user, Client client, String atHash) { + private JwtClaims createIdTokenClaims(User user, Client client, String atHash, String nonce) { JwtClaims claims = new JwtClaims(); // required claims: @@ -179,7 +179,7 @@ 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 - client.nonce().ifPresent(nonce -> claims.setClaim(NONCE, nonce)); + if (nonce != null) 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 6a04d50..39f4cf5 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 @@ -72,15 +72,16 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe var clients = authorizations.getJSONObject(userId); var clientIds = Set.copyOf(clients.keySet()); for (var clientId : clientIds) { - var client = clients.getJSONObject(clientId); - var scopes = Set.copyOf(client.keySet()); + var clientData = clients.getJSONObject(clientId); + var scopeMap = clientData.getJSONObject(SCOPE); + var scopes = Set.copyOf(scopeMap.keySet()); for (var scope : scopes) { - var expiration = Instant.ofEpochSecond(client.getLong(scope)); + var expiration = Instant.ofEpochSecond(scopeMap.getLong(scope)); if (expiration.isBefore(now)) { - client.remove(scope); + scopeMap.remove(scope); } } - if (client.isEmpty()) clients.remove(clientId); + if (scopeMap.isEmpty()) clients.remove(clientId); } if (clients.isEmpty()) authorizations.remove(userId); } @@ -297,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(), Map.of(NAME, client.name(), SECRET, client.secret(), REDIRECT_URIS, client.redirectUris())); + json.getJSONObject(CLIENTS).put(client.id(), client.map()); save(); return this; } @@ -318,13 +319,20 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } @Override - public AuthorizationService authorize(String userId, String clientId, Collection scopes, Instant expiration) { + public AuthorizationService authorize(String userId, String clientId, Collection scopes, String nonce, 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 clientScopes = userAuthorizations.getJSONObject(clientId); + 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); for (var scope : scopes) clientScopes.put(scope, expiration.getEpochSecond()); save(); return this; @@ -342,7 +350,9 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe var authorizations = json.getJSONObject(AUTHORIZATIONS); var userAuthorizations = authorizations.has(userId) ? authorizations.getJSONObject(userId) : null; if (userAuthorizations == null) return unauthorized(scopes); - var clientScopes = userAuthorizations.has(clientId) ? userAuthorizations.getJSONObject(clientId) : null; + var clientData = userAuthorizations.has(clientId) ? userAuthorizations.getJSONObject(clientId) : null; + if (clientData == null) return unauthorized(scopes); + var clientScopes = clientData.has(SCOPE) ? clientData.getJSONObject(SCOPE) : null; if (clientScopes == null) return unauthorized(scopes); var now = Instant.now(); var authorizedScopes = new HashSet(); @@ -361,7 +371,8 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } if (authorizedScopes.isEmpty()) return unauthorized(scopes); - var authorization = new Authorization(clientId, userId, new AuthorizedScopes(authorizedScopes, earliestExpiration)); + String nonce = clientData.has(NONCE) ? clientData.getString(NONCE) : null; + var authorization = new Authorization(clientId, userId, new AuthorizedScopes(authorizedScopes, earliestExpiration), nonce); return new AuthResult(authorization.scopes(), unauthorizedScopes, authCode(authorization)); } 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 9180417..5b2c749 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,8 +21,8 @@ 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, 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 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 SELECT_AUTH = "SELECT * FROM authorizations WHERE userid = ? AND clientId = ? AND scope IN"; private Map authCodes = new HashMap<>(); @@ -76,14 +76,16 @@ public class SqliteAuthService extends SqliteStore implements AuthorizationServi } @Override - public AuthorizationService authorize(String userId, String clientId, Collection scopes, Instant expiration) { + public AuthorizationService authorize(String userId, String clientId, Collection scopes, String nonce, Instant expiration) { try { conn.setAutoCommit(false); var stmt = conn.prepareStatement(SAVE_AUTHORIZATION); stmt.setString(1, userId); stmt.setString(2, clientId); - stmt.setLong(4, expiration.getEpochSecond()); + stmt.setString(4, nonce); stmt.setLong(5, expiration.getEpochSecond()); + stmt.setString(6, nonce); + stmt.setLong(7, expiration.getEpochSecond()); for (var scope : scopes) { stmt.setString(3, scope); stmt.execute(); @@ -128,8 +130,9 @@ public class SqliteAuthService extends SqliteStore implements AuthorizationServi } rs.close(); if (authorized.isEmpty()) return new AuthResult(null, unauthorized, null); - var authorizedScopes = new AuthorizedScopes(authorized, earliestExp); - var authorization = new Authorization(clientId, userId, authorizedScopes); + var authorizedScopes = new AuthorizedScopes(authorized, earliestExp); + String nonce = null; + var authorization = new Authorization(clientId, userId, authorizedScopes, nonce); return new AuthResult(authorizedScopes, unauthorized, authCode(authorization)); } catch (SQLException e) { throw new RuntimeException(e);