diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthResult.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthResult.java new file mode 100644 index 0000000..f1ebaaf --- /dev/null +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthResult.java @@ -0,0 +1,7 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.api; + +import java.util.Set; + +public record AuthResult(AuthorizedScopes authorizedScopes, Set unauthorizedScopes, String authCode) { +} \ No newline at end of file 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..dfdc76d --- /dev/null +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Authorization.java @@ -0,0 +1,5 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.api; + +public record Authorization(String clientId, String userId, AuthorizedScopes scopes) { +} \ No newline at end of file diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizedScope.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizedScope.java deleted file mode 100644 index 42b20c5..0000000 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizedScope.java +++ /dev/null @@ -1,7 +0,0 @@ -/* © SRSoftware 2024 */ -package de.srsoftware.oidc.api; - -import java.time.Instant; - -public record AuthorizedScope(String scope, Instant expiration) { -} diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizedScopes.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizedScopes.java new file mode 100644 index 0000000..03ebcb4 --- /dev/null +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizedScopes.java @@ -0,0 +1,8 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.api; + +import java.time.Instant; +import java.util.Set; + +public record AuthorizedScopes(Set scopes, Instant expiration) { +} diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/ClaimAuthorizationService.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/ClaimAuthorizationService.java index f68c916..bf9d7f9 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/ClaimAuthorizationService.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/ClaimAuthorizationService.java @@ -3,12 +3,10 @@ package de.srsoftware.oidc.api; import java.time.Instant; import java.util.Collection; -import java.util.List; -import java.util.Set; +import java.util.Optional; public interface ClaimAuthorizationService { - public record AuthResult(List authorizedScopes, Set unauthorizedScopes, String authCode) { - } - AuthResult getAuthorization(User user, Client client, Collection scopes); ClaimAuthorizationService authorize(User user, Client client, Collection scopes, Instant expiration); + Optional consumeAuthorization(String authCode); + AuthResult getAuthorization(User user, Client client, Collection scopes); } 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 76e15c5..ecf933f 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 @@ -1,40 +1,44 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.api; + public class Constants { - public static final String ACCESS_TOKEN = "access_token"; - public static final String APP_NAME = "LightOIDC"; - public static final String AUTH_CODE = "authorization_code"; - public static final String AUTHORIZATION = "Authorization"; - public static final String AUTHORZED = "authorized"; - 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 ERROR = "error"; - public static final String CONFIG_PATH = "LIGHTOIDC_CONFIG_PATH"; - public static final String CONFIRMED = "confirmed"; - public static final String DAYS = "days"; - public static final String ERROR_DESCRIPTION = "error_description"; - public static final String EXPIRATION = "expiration"; - public static final String EXPIRES_IN = "expires_in"; - public static final String GRANT_TYPE = "grant_type"; - public static final String ID_TOKEN = "id_token"; - public static final String INVALID_REDIRECT_URI = "invalid_request_uri"; - public static final String INVALID_REQUEST = "invalid_request"; + public static final String ACCESS_TOKEN = "access_token"; + public static final String APP_NAME = "LightOIDC"; + public static final String AUTH_CODE = "authorization_code"; + public static final String AUTHORIZATION = "Authorization"; + public static final String AUTHORZED = "authorized"; + 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 ERROR = "error"; + public static final String CONFIG_PATH = "LIGHTOIDC_CONFIG_PATH"; + public static final String CONFIRMED = "confirmed"; + public static final String DAYS = "days"; + public static final String ERROR_DESCRIPTION = "error_description"; + public static final String EXPIRATION = "expiration"; + public static final String EXPIRES_IN = "expires_in"; + public static final String GRANT_TYPE = "grant_type"; + public static final String ID_TOKEN = "id_token"; + public static final String INVALID_CLIENT = "invalid_client"; + public static final String INVALID_GRANT = "invalid_grant"; + public static final String INVALID_REDIRECT_URI = "invalid_request_uri"; + public static final String INVALID_REQUEST = "invalid_request"; public static final String INVALID_REQUEST_OBJECT = "invalid_request_object"; - public static final String INVALID_SCOPE = "invalid_scope"; - public static final String NAME = "name"; - public static final String NONCE = "nonce"; - public static final String OPENID = "openid"; - public static final String REDIRECT_URI = "redirect_uri"; - public static final String REDIRECT_URIS = "redirect_uris"; - public static final String REQUEST_NOT_SUPPORTED = "request_not_supported"; - public static final String RESPONSE_TYPE = "response_type"; - public static final String SCOPE = "scope"; - public static final String SECRET = "secret"; - public static final String STATE = "state"; - public static final String TOKEN = "token"; - public static final String TOKEN_TYPE = "token_type"; + public static final String INVALID_SCOPE = "invalid_scope"; + public static final String NAME = "name"; + public static final String NONCE = "nonce"; + public static final String OPENID = "openid"; + public static final String REDIRECT_URI = "redirect_uri"; + public static final String REDIRECT_URIS = "redirect_uris"; + public static final String REQUEST_NOT_SUPPORTED = "request_not_supported"; + public static final String RESPONSE_TYPE = "response_type"; + public static final String SCOPE = "scope"; + public static final String SECRET = "secret"; + public static final String STATE = "state"; + public static final String TOKEN = "token"; + public static final String TOKEN_TYPE = "token_type"; + public static final String UNAUTHORIZED_CLIENT = "unauthorized_client"; } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java index 989e228..1fced82 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java @@ -11,10 +11,7 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Stream; import org.json.JSONObject; @@ -30,6 +27,9 @@ public abstract class PathHandler implements HttpHandler { private String[] paths; + public record BasicAuth(String userId, String pass) { + } + public class Bond { Bond(String[] paths) { PathHandler.this.paths = paths; @@ -102,6 +102,16 @@ public abstract class PathHandler implements HttpHandler { return getHeader(ex, AUTHORIZATION); } + public static Optional getBasicAuth(HttpExchange ex) { + return getAuthToken(ex) + .filter(token -> token.startsWith("Basic ")) // + .map(token -> token.substring(6)) + .map(Base64.getDecoder()::decode) + .map(bytes -> new String(bytes, UTF_8)) + .map(token -> token.split(":", 2)) + .map(arr -> new BasicAuth(arr[0], arr[1])); + } + public static Optional getBearer(HttpExchange ex) { return getAuthToken(ex).filter(token -> token.startsWith("Bearer ")).map(token -> token.substring(7)); } 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 647a2f3..bf98340 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 @@ -3,15 +3,16 @@ package de.srsoftware.oidc.backend; import static de.srsoftware.oidc.api.Constants.*; import static de.srsoftware.oidc.api.Permission.MANAGE_CLIENTS; +import static de.srsoftware.utils.Optionals.emptyIfBlank; import static java.net.HttpURLConnection.*; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.oidc.api.*; +import de.srsoftware.utils.Optionals; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.*; -import java.util.stream.Collectors; import org.json.JSONObject; public class ClientController extends Controller { @@ -27,46 +28,46 @@ public class ClientController extends Controller { private boolean authorizationError(HttpExchange ex, String errorCode, String description, String state) throws IOException { var map = new HashMap(); - map.put(ERROR,errorCode); - if (description != null) map.put(ERROR_DESCRIPTION,description); - if (state != null) map.put(STATE,state); - return badRequest(ex,map); + map.put(ERROR, errorCode); + emptyIfBlank(description).ifPresent(d -> map.put(ERROR_DESCRIPTION, d)); + emptyIfBlank(state).ifPresent(s -> map.put(STATE, s)); + return badRequest(ex, map); } private boolean authorize(HttpExchange ex, Session session) throws IOException { - var user = session.user(); - var json = json(ex); + var user = session.user(); + var json = json(ex); var state = json.has(STATE) ? json.getString(STATE) : null; - if (!json.has(CLIENT_ID)) return authorizationError(ex, INVALID_REQUEST,"Missing required parameter \"%s\"!".formatted(CLIENT_ID),state); + if (!json.has(CLIENT_ID)) return authorizationError(ex, INVALID_REQUEST, "Missing required parameter \"%s\"!".formatted(CLIENT_ID), state); var clientId = json.getString(CLIENT_ID); var optClient = clients.getClient(clientId); - if (optClient.isEmpty()) return authorizationError(ex,INVALID_REQUEST_OBJECT,"unknown client: %s".formatted(clientId),state); + if (optClient.isEmpty()) return authorizationError(ex, INVALID_REQUEST_OBJECT, "unknown client: %s".formatted(clientId), state); for (String param : List.of(SCOPE, RESPONSE_TYPE, REDIRECT_URI)) { - if (!json.has(param)) return authorizationError(ex,INVALID_REQUEST,"Missing required parameter \"%s\"!".formatted(param),state); + if (!json.has(param)) return authorizationError(ex, INVALID_REQUEST, "Missing required parameter \"%s\"!".formatted(param), state); } var scopes = toList(json, SCOPE); - if (!scopes.contains(OPENID)) return authorizationError(ex,INVALID_SCOPE,"This is an OpenID Provider. You should request \"openid\" scope!",state); + if (!scopes.contains(OPENID)) return authorizationError(ex, INVALID_SCOPE, "This is an OpenID Provider. You should request \"openid\" scope!", state); var responseTypes = toList(json, RESPONSE_TYPE); for (var responseType : responseTypes) { switch (responseType) { case ID_TOKEN: case TOKEN: - return authorizationError(ex, REQUEST_NOT_SUPPORTED, "Response type \"%s\" currently not supported".formatted(responseType),state); + return authorizationError(ex, REQUEST_NOT_SUPPORTED, "Response type \"%s\" currently not supported".formatted(responseType), state); case CODE: break; default: - return authorizationError(ex,INVALID_REQUEST_OBJECT,"Unknown response type \"%s\"".formatted(responseType),state); + return authorizationError(ex, INVALID_REQUEST_OBJECT, "Unknown response type \"%s\"".formatted(responseType), state); } } - if ( !responseTypes.contains(CODE)) return authorizationError(ex, REQUEST_NOT_SUPPORTED, "Sorry, at the moment I can only handle \"%s\" response type".formatted(CODE),state); + if (!responseTypes.contains(CODE)) return authorizationError(ex, REQUEST_NOT_SUPPORTED, "Sorry, at the moment I can only handle \"%s\" response type".formatted(CODE), state); var client = optClient.get(); 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)) { + if (json.has(AUTHORZED)) { // user did consent var authorized = json.getJSONObject(AUTHORZED); var days = authorized.getInt("days"); var list = new ArrayList(); @@ -78,9 +79,9 @@ public class ClientController extends Controller { if (!authResult.unauthorizedScopes().isEmpty()) { return sendContent(ex, Map.of("unauthorized_scopes", authResult.unauthorizedScopes(), "rp", client.name())); } - var authorizedScopes = authResult.authorizedScopes().stream().map(AuthorizedScope::scope).collect(Collectors.joining(" ")); - var result = new HashMap(); - result.put(SCOPE, authorizedScopes); + var joinedAuthorizedScopes = Optionals.nullable(authResult.authorizedScopes()).map(AuthorizedScopes::scopes).map(list -> String.join(" ", list)); + var result = new HashMap(); + joinedAuthorizedScopes.ifPresent(authorizedScopes -> result.put(SCOPE, authorizedScopes)); result.put(CODE, authResult.authCode()); if (state != null) result.put(STATE, state); return sendContent(ex, result); 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 6180a9d..2a2ac4b 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 @@ -2,18 +2,23 @@ package de.srsoftware.oidc.backend; import static de.srsoftware.oidc.api.Constants.*; +import static de.srsoftware.oidc.api.Constants.ERROR; +import static de.srsoftware.utils.Optionals.emptyIfBlank; import static java.lang.System.Logger.Level.*; -import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import com.sun.net.httpserver.HttpExchange; 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; import org.jose4j.jwk.PublicJsonWebKey; import org.jose4j.jws.JsonWebSignature; import org.jose4j.jwt.JwtClaims; import org.jose4j.lang.JoseException; +import org.json.JSONObject; public class TokenController extends PathHandler { public record Configuration(String issuer, int tokenExpirationMinutes) { @@ -46,36 +51,54 @@ public class TokenController extends PathHandler { return notFound(ex); } + private HashMap tokenResponse(String errorCode, String description) throws IOException { + var map = new HashMap(); + map.put(ERROR, errorCode); + emptyIfBlank(description).ifPresent(d -> map.put(ERROR_DESCRIPTION, d)); + return map; + } + private boolean provideToken(HttpExchange ex) throws IOException { var map = deserialize(body(ex)); - // TODO: check data, → https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint - return sendEmptyResponse(HTTP_NOT_IMPLEMENTED, ex); - /* + var grantType = map.get(GRANT_TYPE); - if (!AUTH_CODE.equals(grantType)) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown grant type", GRANT_TYPE, grantType)); + // verify grant type + if (!AUTH_CODE.equals(grantType)) return badRequest(ex, tokenResponse(INVALID_GRANT, "unknown grant type \"%s\"".formatted(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 basicAuth = getBasicAuth(ex).orElse(null); - 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 clientId = basicAuth != null ? basicAuth.userId() : map.get(CLIENT_ID); var optClient = clients.getClient(clientId); - if (optClient.isEmpty()) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown client", CLIENT_ID, clientId)); + if (optClient.isEmpty()) return badRequest(ex, tokenResponse(INVALID_CLIENT, "unknown client \"%s\"".formatted(clientId))); + var client = optClient.get(); + if (client.secret() != null) { // for confidential clients: + // authenticate client by matching secret + String clientSecret = basicAuth != null ? basicAuth.pass() : map.get(CLIENT_SECRET); + if (clientSecret == null) return sendContent(ex, HTTP_UNAUTHORIZED, tokenResponse(INVALID_CLIENT, "client not authenticated")); + if (!client.secret().equals(clientSecret)) return sendContent(ex, HTTP_UNAUTHORIZED, tokenResponse(INVALID_CLIENT, "client not authenticated")); + } - var user = users.load(authorization.userId()); - if (user.isEmpty()) return sendContent(ex, 500, Map.of(ERROR, "User not found")); + var authCode = map.get(CODE); + // verify that code is not re-used + var optAuthorization = authorizations.consumeAuthorization(authCode); + if (optAuthorization.isEmpty()) return badRequest(ex, tokenResponse(INVALID_GRANT, "invalid auth code: \"%s\"".formatted(authCode))); + var authorization = optAuthorization.get(); + + // verify authorization code was issued to the authenticated client + if (!authorization.clientId().equals(clientId)) return badRequest(ex, tokenResponse(UNAUTHORIZED_CLIENT, null)); + + // verify redirect URI 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)); + if (!client.redirectUris().contains(uri)) return badRequest(ex, tokenResponse(INVALID_REQUEST, "unknown redirect uri: \"%s\"".formatted(uri))); + + // verify user is valid + var user = users.load(authorization.userId()); + if (user.isEmpty()) return badRequest(ex, tokenResponse(INVALID_REQUEST, "unknown user")); + + if (!authorization.scopes().scopes().contains(OPENID)) return badRequest(ex, tokenResponse(INVALID_REQUEST, "Token invalid for OpenID scope")); - if (client.secret() != null) { - String clientSecret = nullable(ex.getRequestHeaders().get(AUTHORIZATION)).map(list -> list.get(0)).filter(s -> s.startsWith("Basic ")).map(s -> s.substring(6)).map(s -> Base64.getDecoder().decode(s)).map(bytes -> new String(bytes, StandardCharsets.UTF_8)).filter(s -> s.startsWith("%s:".formatted(client.id()))).map(s -> s.substring(client.id().length() + 1).trim()).orElseGet(() -> map.get(CLIENT_SECRET)); - if (clientSecret == null) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "client secret missing")); - if (!client.secret().equals(clientSecret)) 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(); @@ -83,8 +106,8 @@ public class TokenController extends PathHandler { response.put(TOKEN_TYPE, BEARER); response.put(EXPIRES_IN, 3600); response.put(ID_TOKEN, jwToken); - LOG.log(DEBUG, jwToken); - return sendContent(ex, response);*/ + + return sendContent(ex, response); } private String createJWT(Client client, User user) { 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 4304045..4f94b02 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,7 +37,7 @@ public class FileStore implements ClaimAuthorizationService, ClientService, Sess private Duration sessionDuration = Duration.of(10, ChronoUnit.MINUTES); private Map clients = new HashMap<>(); private Map accessTokens = new HashMap<>(); - private Map authCodes = new HashMap<>(); + private Map authCodes = new HashMap<>(); public FileStore(File storage, PasswordHasher passwordHasher) throws IOException { this.storageFile = storage.toPath(); @@ -266,6 +266,11 @@ public class FileStore implements ClaimAuthorizationService, ClientService, Sess } /*** ClaimAuthorizationService methods ***/ + private String authCode(User user, Client client, Authorization authorization) { + var code = uuid(); + authCodes.put(code, authorization); + return code; + } @Override public ClaimAuthorizationService authorize(User user, Client client, Collection scopes, Instant expiration) { @@ -280,8 +285,10 @@ public class FileStore implements ClaimAuthorizationService, ClientService, Sess return this; } - private AuthResult unauthorized(Collection scopes) { - return new AuthResult(List.of(), new HashSet<>(scopes), null); + + @Override + public Optional consumeAuthorization(String authCode) { + return nullable(authCodes.remove(authCode)); } @Override @@ -291,25 +298,27 @@ public class FileStore implements ClaimAuthorizationService, ClientService, Sess if (userAuthorizations == null) return unauthorized(scopes); var clientScopes = userAuthorizations.has(client.id()) ? userAuthorizations.getJSONObject(client.id()) : null; if (clientScopes == null) return unauthorized(scopes); - var now = Instant.now(); - var authorizedScopes = new ArrayList(); - var unauthorizedScopes = new HashSet(); + var now = Instant.now(); + var authorizedScopes = new HashSet(); + var unauthorizedScopes = new HashSet(); + Instant earliestExpiration = null; for (var scope : scopes) { if (clientScopes.has(scope)) { var expiration = Instant.ofEpochSecond(clientScopes.getLong(scope)); if (expiration.isAfter(now)) { - authorizedScopes.add(new AuthorizedScope(scope, expiration)); + authorizedScopes.add(scope); + if (earliestExpiration == null || expiration.isBefore(earliestExpiration)) earliestExpiration = expiration; } else { unauthorizedScopes.add(scope); } } } - return new AuthResult(authorizedScopes, unauthorizedScopes, authCode(user, client)); + + var authorization = new Authorization(client.id(), user.uuid(), new AuthorizedScopes(authorizedScopes, earliestExpiration)); + return new AuthResult(authorization.scopes(), unauthorizedScopes, authCode(user, client, authorization)); } - private String authCode(User user, Client client) { - var code = uuid(); - authCodes.put(user.uuid() + "@" + client.id(), code); - return code; + private AuthResult unauthorized(Collection scopes) { + return new AuthResult(null, new HashSet<>(scopes), null); } }