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;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface AuthorizationService {
|
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);
|
Optional<Authorization> consumeAuthorization(String authCode);
|
||||||
AuthResult getAuthorization(String userId, String clientId, Collection<String> scopes);
|
AuthResult getAuthorization(String userId, String clientId, Collection<String> scopes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.oidc.api.data;
|
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.oidc.api.Constants.*;
|
||||||
import static de.srsoftware.utils.Optionals.nullable;
|
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public final class Client {
|
public final class Client {
|
||||||
private static System.Logger LOG = System.getLogger(Client.class.getSimpleName());
|
private static System.Logger LOG = System.getLogger(Client.class.getSimpleName());
|
||||||
private final String id, name, secret;
|
private final String id, name, secret;
|
||||||
private String nonce = null;
|
|
||||||
private final Set<String> redirectUris;
|
private final Set<String> redirectUris;
|
||||||
|
|
||||||
public Client(String id, String name, String secret, Set<String> redirectUris) {
|
public Client(String id, String name, String secret, Set<String> redirectUris) {
|
||||||
@@ -33,16 +31,6 @@ public final class Client {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Client nonce(String newVal) {
|
|
||||||
nonce = newVal;
|
|
||||||
;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional nonce() {
|
|
||||||
return nullable(nonce);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String secret() {
|
public String secret() {
|
||||||
return secret;
|
return secret;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ public abstract class AuthServiceTest {
|
|||||||
var authorizationService = authorizationService();
|
var authorizationService = authorizationService();
|
||||||
var userId1 = uuid();
|
var userId1 = uuid();
|
||||||
var expiration = Instant.now();
|
var expiration = Instant.now();
|
||||||
authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration);
|
var nonce = uuid();
|
||||||
expiration = Instant.now().plusSeconds(3600).truncatedTo(SECONDS); // test overwrite
|
authorizationService.authorize(userId1, CLIENT1, SCOPES1, nonce, expiration);
|
||||||
authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration); // test overwrite
|
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));
|
var authorization = authorizationService.getAuthorization(userId1, CLIENT1, Set.of(OPENID));
|
||||||
assertEquals(1, authorization.authorizedScopes().scopes().size());
|
assertEquals(1, authorization.authorizedScopes().scopes().size());
|
||||||
assertTrue(authorization.authorizedScopes().scopes().contains(OPENID));
|
assertTrue(authorization.authorizedScopes().scopes().contains(OPENID));
|
||||||
@@ -52,9 +53,10 @@ public abstract class AuthServiceTest {
|
|||||||
public void testConsume() {
|
public void testConsume() {
|
||||||
var authorizationService = authorizationService();
|
var authorizationService = authorizationService();
|
||||||
|
|
||||||
|
var nonce = uuid();
|
||||||
var userId1 = uuid();
|
var userId1 = uuid();
|
||||||
var expiration = Instant.now().plusSeconds(3600).truncatedTo(SECONDS);
|
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 authResult = authorizationService.getAuthorization(userId1, CLIENT1, Set.of(OPENID));
|
||||||
var authCode = authResult.authCode();
|
var authCode = authResult.authCode();
|
||||||
assertNotNull(authCode);
|
assertNotNull(authCode);
|
||||||
@@ -72,4 +74,6 @@ public abstract class AuthServiceTest {
|
|||||||
optAuth = authorizationService.consumeAuthorization(authCode);
|
optAuth = authorizationService.consumeAuthorization(authCode);
|
||||||
assertTrue(optAuth.isEmpty());
|
assertTrue(optAuth.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: test nonce passing
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,13 +73,14 @@ public class ClientController extends Controller {
|
|||||||
var redirect = json.getString(REDIRECT_URI);
|
var redirect = json.getString(REDIRECT_URI);
|
||||||
if (!client.redirectUris().contains(redirect)) authorizationError(ex, INVALID_REDIRECT_URI, "unknown redirect uri: %s".formatted(redirect), state);
|
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
|
if (json.has(AUTHORZED)) { // user did consent
|
||||||
var authorized = json.getJSONObject(AUTHORZED);
|
var authorized = json.getJSONObject(AUTHORZED);
|
||||||
var days = authorized.getInt("days");
|
var days = authorized.getInt("days");
|
||||||
var list = new ArrayList<String>();
|
var list = new ArrayList<String>();
|
||||||
authorized.getJSONArray("scopes").forEach(scope -> list.add(scope.toString()));
|
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);
|
var authResult = authorizations.getAuthorization(user.uuid(), client.id(), scopes);
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public class TokenController extends PathHandler {
|
|||||||
var user = optUser.get();
|
var user = optUser.get();
|
||||||
|
|
||||||
var accessToken = users.accessToken(user);
|
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");
|
ex.getResponseHeaders().add("Cache-Control", "no-store");
|
||||||
JSONObject response = new JSONObject();
|
JSONObject response = new JSONObject();
|
||||||
response.put(ACCESS_TOKEN, accessToken.id());
|
response.put(ACCESS_TOKEN, accessToken.id());
|
||||||
@@ -126,13 +126,13 @@ public class TokenController extends PathHandler {
|
|||||||
return sendContent(ex, response);
|
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 {
|
try {
|
||||||
PublicJsonWebKey key = keyManager.getKey();
|
PublicJsonWebKey key = keyManager.getKey();
|
||||||
var algo = key.getAlgorithm();
|
var algo = key.getAlgorithm();
|
||||||
var atHash = this.atHash(algo, accessToken);
|
var atHash = this.atHash(algo, accessToken);
|
||||||
key.setUse("sig");
|
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.
|
// 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.
|
// 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();
|
JwtClaims claims = new JwtClaims();
|
||||||
|
|
||||||
// required claims:
|
// required claims:
|
||||||
@@ -179,7 +179,7 @@ public class TokenController extends PathHandler {
|
|||||||
claims.setClaim(AT_HASH, atHash);
|
claims.setClaim(AT_HASH, atHash);
|
||||||
claims.setClaim(CLIENT_ID, client.id());
|
claims.setClaim(CLIENT_ID, client.id());
|
||||||
claims.setClaim(EMAIL, user.email()); // additional claims/attributes about the subject can be added
|
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
|
claims.setGeneratedJwtId(); // a unique identifier for the token
|
||||||
return claims;
|
return claims;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,15 +72,16 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
var clients = authorizations.getJSONObject(userId);
|
var clients = authorizations.getJSONObject(userId);
|
||||||
var clientIds = Set.copyOf(clients.keySet());
|
var clientIds = Set.copyOf(clients.keySet());
|
||||||
for (var clientId : clientIds) {
|
for (var clientId : clientIds) {
|
||||||
var client = clients.getJSONObject(clientId);
|
var clientData = clients.getJSONObject(clientId);
|
||||||
var scopes = Set.copyOf(client.keySet());
|
var scopeMap = clientData.getJSONObject(SCOPE);
|
||||||
|
var scopes = Set.copyOf(scopeMap.keySet());
|
||||||
for (var scope : scopes) {
|
for (var scope : scopes) {
|
||||||
var expiration = Instant.ofEpochSecond(client.getLong(scope));
|
var expiration = Instant.ofEpochSecond(scopeMap.getLong(scope));
|
||||||
if (expiration.isBefore(now)) {
|
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);
|
if (clients.isEmpty()) authorizations.remove(userId);
|
||||||
}
|
}
|
||||||
@@ -297,7 +298,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
@Override
|
@Override
|
||||||
public ClientService save(Client client) {
|
public ClientService save(Client client) {
|
||||||
if (!json.has(CLIENTS)) json.put(CLIENTS, new JSONObject());
|
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();
|
save();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -318,13 +319,20 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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());
|
if (!json.has(AUTHORIZATIONS)) json.put(AUTHORIZATIONS, new JSONObject());
|
||||||
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
||||||
if (!authorizations.has(userId)) authorizations.put(userId, new JSONObject());
|
if (!authorizations.has(userId)) authorizations.put(userId, new JSONObject());
|
||||||
var userAuthorizations = authorizations.getJSONObject(userId);
|
var userAuthorizations = authorizations.getJSONObject(userId);
|
||||||
if (!userAuthorizations.has(clientId)) userAuthorizations.put(clientId, new JSONObject());
|
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());
|
for (var scope : scopes) clientScopes.put(scope, expiration.getEpochSecond());
|
||||||
save();
|
save();
|
||||||
return this;
|
return this;
|
||||||
@@ -342,7 +350,9 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
||||||
var userAuthorizations = authorizations.has(userId) ? authorizations.getJSONObject(userId) : null;
|
var userAuthorizations = authorizations.has(userId) ? authorizations.getJSONObject(userId) : null;
|
||||||
if (userAuthorizations == null) return unauthorized(scopes);
|
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);
|
if (clientScopes == null) return unauthorized(scopes);
|
||||||
var now = Instant.now();
|
var now = Instant.now();
|
||||||
var authorizedScopes = new HashSet<String>();
|
var authorizedScopes = new HashSet<String>();
|
||||||
@@ -361,7 +371,8 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
}
|
}
|
||||||
if (authorizedScopes.isEmpty()) return unauthorized(scopes);
|
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));
|
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 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 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 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, expiration) VALUES (?,?,?,?) ON CONFLICT DO UPDATE SET expiration = ?";
|
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 static final String SELECT_AUTH = "SELECT * FROM authorizations WHERE userid = ? AND clientId = ? AND scope IN";
|
||||||
private Map<String, Authorization> authCodes = new HashMap<>();
|
private Map<String, Authorization> authCodes = new HashMap<>();
|
||||||
|
|
||||||
@@ -76,14 +76,16 @@ public class SqliteAuthService extends SqliteStore implements AuthorizationServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 {
|
try {
|
||||||
conn.setAutoCommit(false);
|
conn.setAutoCommit(false);
|
||||||
var stmt = conn.prepareStatement(SAVE_AUTHORIZATION);
|
var stmt = conn.prepareStatement(SAVE_AUTHORIZATION);
|
||||||
stmt.setString(1, userId);
|
stmt.setString(1, userId);
|
||||||
stmt.setString(2, clientId);
|
stmt.setString(2, clientId);
|
||||||
stmt.setLong(4, expiration.getEpochSecond());
|
stmt.setString(4, nonce);
|
||||||
stmt.setLong(5, expiration.getEpochSecond());
|
stmt.setLong(5, expiration.getEpochSecond());
|
||||||
|
stmt.setString(6, nonce);
|
||||||
|
stmt.setLong(7, expiration.getEpochSecond());
|
||||||
for (var scope : scopes) {
|
for (var scope : scopes) {
|
||||||
stmt.setString(3, scope);
|
stmt.setString(3, scope);
|
||||||
stmt.execute();
|
stmt.execute();
|
||||||
@@ -128,8 +130,9 @@ public class SqliteAuthService extends SqliteStore implements AuthorizationServi
|
|||||||
}
|
}
|
||||||
rs.close();
|
rs.close();
|
||||||
if (authorized.isEmpty()) return new AuthResult(null, unauthorized, null);
|
if (authorized.isEmpty()) return new AuthResult(null, unauthorized, null);
|
||||||
var authorizedScopes = new AuthorizedScopes(authorized, earliestExp);
|
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));
|
return new AuthResult(authorizedScopes, unauthorized, authCode(authorization));
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|||||||
Reference in New Issue
Block a user