moved nonce from client to auhtorization
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -8,7 +8,7 @@ import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AuthorizationService {
|
||||
AuthorizationService authorize(String userId, String clientId, Collection<String> scopes, Instant expiration);
|
||||
AuthorizationService authorize(String userId, String clientId, Collection<String> scopes, String nonce, Instant expiration);
|
||||
Optional<Authorization> consumeAuthorization(String authCode);
|
||||
AuthResult getAuthorization(String userId, String clientId, Collection<String> scopes);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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<String> redirectUris;
|
||||
|
||||
public Client(String id, String name, String secret, Set<String> 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;
|
||||
}
|
||||
|
||||
@@ -26,9 +26,10 @@ public abstract class AuthServiceTest {
|
||||
var authorizationService = authorizationService();
|
||||
var userId1 = uuid();
|
||||
var expiration = Instant.now();
|
||||
authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration);
|
||||
var nonce = uuid();
|
||||
authorizationService.authorize(userId1, CLIENT1, SCOPES1, nonce, expiration);
|
||||
expiration = Instant.now().plusSeconds(3600).truncatedTo(SECONDS); // test overwrite
|
||||
authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration); // 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
|
||||
}
|
||||
|
||||
@@ -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<String>();
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<String> scopes, Instant expiration) {
|
||||
public AuthorizationService authorize(String userId, String clientId, Collection<String> 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<String>();
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, Authorization> authCodes = new HashMap<>();
|
||||
|
||||
@@ -76,14 +76,16 @@ public class SqliteAuthService extends SqliteStore implements AuthorizationServi
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationService authorize(String userId, String clientId, Collection<String> scopes, Instant expiration) {
|
||||
public AuthorizationService authorize(String userId, String clientId, Collection<String> 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();
|
||||
@@ -129,7 +131,8 @@ 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);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user