revised TokenController.provideToken
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -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<String, String>();
|
||||
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<String>();
|
||||
@@ -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<String, String>();
|
||||
result.put(SCOPE, authorizedScopes);
|
||||
var joinedAuthorizedScopes = Optionals.nullable(authResult.authorizedScopes()).map(AuthorizedScopes::scopes).map(list -> String.join(" ", list));
|
||||
var result = new HashMap<String, String>();
|
||||
joinedAuthorizedScopes.ifPresent(authorizedScopes -> result.put(SCOPE, authorizedScopes));
|
||||
result.put(CODE, authResult.authCode());
|
||||
if (state != null) result.put(STATE, state);
|
||||
return sendContent(ex, result);
|
||||
|
||||
@@ -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<String, String> tokenResponse(String errorCode, String description) throws IOException {
|
||||
var map = new HashMap<String, String>();
|
||||
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));
|
||||
|
||||
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 grantType = map.get(GRANT_TYPE);
|
||||
// verify grant type
|
||||
if (!AUTH_CODE.equals(grantType)) return badRequest(ex, tokenResponse(INVALID_GRANT, "unknown grant type \"%s\"".formatted(grantType)));
|
||||
|
||||
var basicAuth = getBasicAuth(ex).orElse(null);
|
||||
|
||||
var clientId = basicAuth != null ? basicAuth.userId() : map.get(CLIENT_ID);
|
||||
var optClient = clients.getClient(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 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();
|
||||
|
||||
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 optClient = clients.getClient(clientId);
|
||||
if (optClient.isEmpty()) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown client", CLIENT_ID, clientId));
|
||||
var client = optClient.get();
|
||||
|
||||
var user = users.load(authorization.userId());
|
||||
if (user.isEmpty()) return sendContent(ex, 500, Map.of(ERROR, "User not found"));
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user