implemented main part of authorization and token delivery

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2024-07-30 00:22:21 +02:00
parent 6b7e0d2c97
commit 1e8ca6dc3a
20 changed files with 296 additions and 126 deletions

View File

@@ -9,8 +9,11 @@ import static java.net.HttpURLConnection.*;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.oidc.api.*;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.HashSet;
import java.util.Map;
import java.util.UUID;
import org.json.JSONObject;
public class ClientController extends Controller {
@@ -24,17 +27,6 @@ public class ClientController extends Controller {
clients = clientService;
}
private boolean add(HttpExchange ex, Session session) throws IOException {
if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED");
var json = json(ex);
var redirects = new HashSet<String>();
for (Object o : json.getJSONArray(REDIRECT_URIS)) {
if (o instanceof String s) redirects.add(s);
}
var client = new Client(json.getString(CLIENT_ID), json.getString(NAME), json.getString(SECRET), redirects);
clients.add(client);
return sendContent(ex, client);
}
private boolean authorize(HttpExchange ex, Session session) throws IOException {
var user = session.user();
@@ -48,14 +40,17 @@ public class ClientController extends Controller {
if (!client.redirectUris().contains(redirect)) return badRequest(ex, Map.of(CAUSE, "unknown redirect uri", REDIRECT_URI, redirect));
if (!authorizations.isAuthorized(client, session.user())) {
if (json.has(CONFIRMED) && json.getBoolean(CONFIRMED)) {
authorizations.authorize(client, user, null);
if (json.has(DAYS)) {
var days = json.getInt(DAYS);
var expiration = Instant.now().plus(Duration.ofDays(days));
authorizations.authorize(client, user, expiration);
} else {
return sendContent(ex, Map.of(CONFIRMED, false, NAME, client.name()));
}
}
var state = json.getString(STATE);
var code = client.generateCode();
var code = UUID.randomUUID().toString();
authorizations.addCode(client, session.user(), code);
return sendContent(ex, Map.of(CONFIRMED, true, CODE, code, REDIRECT_URI, redirect, STATE, state));
}
@@ -94,8 +89,8 @@ public class ClientController extends Controller {
switch (path) {
case "/":
return load(ex, session);
case "/add":
return add(ex, session);
case "/add", "/update":
return save(ex, session);
case "/authorize":
return authorize(ex, session);
case "/list":
@@ -123,4 +118,16 @@ public class ClientController extends Controller {
}
return sendEmptyResponse(HTTP_NOT_FOUND, ex);
}
private boolean save(HttpExchange ex, Session session) throws IOException {
if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED");
var json = json(ex);
var redirects = new HashSet<String>();
for (Object o : json.getJSONArray(REDIRECT_URIS)) {
if (o instanceof String s) redirects.add(s);
}
var client = new Client(json.getString(CLIENT_ID), json.getString(NAME), json.getString(SECRET), redirects);
clients.save(client);
return sendContent(ex, client);
}
}

View File

@@ -6,10 +6,9 @@ import static java.lang.System.Logger.Level.*;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.oidc.api.Client;
import de.srsoftware.oidc.api.ClientService;
import de.srsoftware.oidc.api.PathHandler;
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;
@@ -21,10 +20,14 @@ import org.jose4j.lang.JoseException;
import org.json.JSONObject;
public class TokenController extends PathHandler {
private final ClientService clients;
private final ClientService clients;
private final AuthorizationService authorizations;
private final UserService users;
public TokenController(ClientService clientService) {
clients = clientService;
public TokenController(AuthorizationService authorizationService, ClientService clientService, UserService userService) {
authorizations = authorizationService;
clients = clientService;
users = userService;
}
private Map<String, String> deserialize(String body) {
@@ -42,27 +45,31 @@ public class TokenController extends PathHandler {
}
private boolean provideToken(HttpExchange ex) throws IOException {
var map = deserialize(body(ex));
// TODO: check Authorization Code, → https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
// TODO: check Redirect URL
LOG.log(DEBUG, "post data: {0}", map);
LOG.log(WARNING, "{0}.provideToken(ex) not implemented!", getClass().getSimpleName());
var map = deserialize(body(ex));
var grantType = map.get(GRANT_TYPE);
if (!ATUH_CODE.equals(grantType)) sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown grant type", GRANT_TYPE, grantType));
var optClient = Optional.ofNullable(map.get(CLIENT_ID)).flatMap(clients::getClient);
if (optClient.isEmpty()) {
LOG.log(ERROR, "client not found");
return sendEmptyResponse(HTTP_BAD_REQUEST, ex);
// TODO: send correct response
}
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 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"));
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));
var secretFromClient = map.get(CLIENT_SECRET);
var client = optClient.get();
if (!client.secret().equals(secretFromClient)) {
LOG.log(ERROR, "client secret mismatch");
return sendEmptyResponse(HTTP_BAD_REQUEST, ex);
// TODO: send correct response
}
String jwToken = createJWT(client);
if (!client.secret().equals(secretFromClient)) 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();
response.put(ACCESS_TOKEN, UUID.randomUUID().toString()); // TODO: wofür genau wird der verwendet, was gilt es hier zu beachten
@@ -72,22 +79,12 @@ public class TokenController extends PathHandler {
return sendContent(ex, response);
}
private String createJWT(Client client) {
private String createJWT(Client client, User user) {
try {
byte[] secretBytes = client.secret().getBytes(StandardCharsets.UTF_8);
HmacKey hmacKey = new HmacKey(secretBytes);
JwtClaims claims = new JwtClaims();
claims.setIssuer("Issuer"); // who creates the token and signs it
claims.setAudience("Audience"); // to whom the token is intended to be sent
claims.setExpirationTimeMinutesInTheFuture(10); // time when the token will expire (10 minutes from now)
claims.setGeneratedJwtId(); // a unique identifier for the token
claims.setIssuedAtToNow(); // when the token was issued/created (now)
claims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago)
claims.setSubject("subject"); // the subject/principal is whom the token is about
claims.setClaim("email", "mail@example.com"); // additional claims/attributes about the subject can be added
List<String> groups = Arrays.asList("group-one", "other-group", "group-three");
claims.setStringListClaim("groups", groups); // multi-valued claims work too and will end up as a JSON array
JwtClaims claims = getJwtClaims(user);
// 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.
@@ -105,4 +102,16 @@ public class TokenController extends PathHandler {
throw new RuntimeException(e);
}
}
private static JwtClaims getJwtClaims(User user) {
JwtClaims claims = new JwtClaims();
claims.setIssuer(APP_NAME); // who creates the token and signs it
claims.setExpirationTimeMinutesInTheFuture(10); // time when the token will expire (10 minutes from now)
claims.setGeneratedJwtId(); // a unique identifier for the token
claims.setIssuedAtToNow(); // when the token was issued/created (now)
claims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago)
claims.setSubject(user.uuid()); // the subject/principal is whom the token is about
claims.setClaim("email", user.email()); // additional claims/attributes about the subject can be added
return claims;
}
}