Browse Source

implementing at_hash

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 3 months ago
parent
commit
d5ff936710
  1. 2
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java
  2. 2
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java
  3. 52
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java
  4. 1
      de.srsoftware.oidc.web/src/main/resources/en/navigation.html
  5. 1
      de.srsoftware.oidc.web/src/main/resources/en/todo.html

2
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java

@ -5,6 +5,7 @@ package de.srsoftware.oidc.api;
public class Constants { public class Constants {
public static final String ACCESS_TOKEN = "access_token"; public static final String ACCESS_TOKEN = "access_token";
public static final String APP_NAME = "LightOIDC"; public static final String APP_NAME = "LightOIDC";
public static final String AT_HASH = "at_hash";
public static final String AUTH_CODE = "authorization_code"; public static final String AUTH_CODE = "authorization_code";
public static final String AUTHORZED = "authorized"; public static final String AUTHORZED = "authorized";
public static final String BEARER = "Bearer"; public static final String BEARER = "Bearer";
@ -12,6 +13,7 @@ public class Constants {
public static final String CLIENT_ID = "client_id"; public static final String CLIENT_ID = "client_id";
public static final String CLIENT_SECRET = "client_secret"; public static final String CLIENT_SECRET = "client_secret";
public static final String CODE = "code"; public static final String CODE = "code";
public static final String EMAIL = "email";
public static final String ERROR = "error"; public static final String ERROR = "error";
public static final String CONFIG_PATH = "LIGHTOIDC_CONFIG_PATH"; public static final String CONFIG_PATH = "LIGHTOIDC_CONFIG_PATH";
public static final String CONFIRMED = "confirmed"; public static final String CONFIRMED = "confirmed";

2
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java

@ -1,6 +1,7 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.oidc.api.data; package de.srsoftware.oidc.api.data;
import static de.srsoftware.oidc.api.Constants.EMAIL;
import static de.srsoftware.oidc.api.Constants.SESSION_DURATION; import static de.srsoftware.oidc.api.Constants.SESSION_DURATION;
import java.time.Duration; import java.time.Duration;
@ -8,7 +9,6 @@ import java.util.*;
import org.json.JSONObject; import org.json.JSONObject;
public final class User { public final class User {
public static final String EMAIL = "email";
public static final String PASSWORD = "password"; public static final String PASSWORD = "password";
public static final String PERMISSIONS = "permissions"; public static final String PERMISSIONS = "permissions";
public static final String REALNAME = "realname"; public static final String REALNAME = "realname";

52
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java

@ -5,15 +5,19 @@ import static de.srsoftware.oidc.api.Constants.*;
import static de.srsoftware.oidc.api.Constants.ERROR; import static de.srsoftware.oidc.api.Constants.ERROR;
import static de.srsoftware.utils.Optionals.emptyIfBlank; import static de.srsoftware.utils.Optionals.emptyIfBlank;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.http.PathHandler; import de.srsoftware.http.PathHandler;
import de.srsoftware.oidc.api.*; import de.srsoftware.oidc.api.*;
import de.srsoftware.oidc.api.data.AccessToken;
import de.srsoftware.oidc.api.data.Client; import de.srsoftware.oidc.api.data.Client;
import de.srsoftware.oidc.api.data.User; import de.srsoftware.oidc.api.data.User;
import java.io.IOException; import java.io.IOException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jose4j.jwk.PublicJsonWebKey; import org.jose4j.jwk.PublicJsonWebKey;
@ -30,6 +34,7 @@ public class TokenController extends PathHandler {
private final UserService users; private final UserService users;
private final KeyManager keyManager; private final KeyManager keyManager;
private Configuration config; private Configuration config;
private static final Base64.Encoder BASE64 = Base64.getUrlEncoder();
public TokenController(AuthorizationService authorizationService, ClientService clientService, KeyManager keyManager, UserService userService, Configuration configuration) { public TokenController(AuthorizationService authorizationService, ClientService clientService, KeyManager keyManager, UserService userService, Configuration configuration) {
authorizations = authorizationService; authorizations = authorizationService;
@ -103,15 +108,17 @@ public class TokenController extends PathHandler {
if (!client.redirectUris().contains(uri)) return badRequest(ex, tokenResponse(INVALID_REQUEST, "unknown redirect uri: \"%s\"".formatted(uri))); if (!client.redirectUris().contains(uri)) return badRequest(ex, tokenResponse(INVALID_REQUEST, "unknown redirect uri: \"%s\"".formatted(uri)));
// verify user is valid // verify user is valid
var user = users.load(authorization.userId()); var optUser = users.load(authorization.userId());
if (user.isEmpty()) return badRequest(ex, tokenResponse(INVALID_REQUEST, "unknown user")); if (optUser.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 (!authorization.scopes().scopes().contains(OPENID)) return badRequest(ex, tokenResponse(INVALID_REQUEST, "Token invalid for OpenID scope"));
var user = optUser.get();
String jwToken = createJWT(client, user.get()); var accessToken = users.accessToken(user);
String jwToken = createJWT(client, user, accessToken);
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, users.accessToken(user.get()).id()); response.put(ACCESS_TOKEN, accessToken.id());
response.put(TOKEN_TYPE, BEARER); response.put(TOKEN_TYPE, BEARER);
response.put(EXPIRES_IN, 3600); response.put(EXPIRES_IN, 3600);
response.put(ID_TOKEN, jwToken); response.put(ID_TOKEN, jwToken);
@ -119,11 +126,13 @@ public class TokenController extends PathHandler {
return sendContent(ex, response); return sendContent(ex, response);
} }
private String createJWT(Client client, User user) { private String createJWT(Client client, User user, AccessToken accessToken) {
try { try {
PublicJsonWebKey key = keyManager.getKey(); PublicJsonWebKey key = keyManager.getKey();
var algo = key.getAlgorithm();
var atHash = this.atHash(algo, accessToken);
key.setUse("sig"); key.setUse("sig");
JwtClaims claims = createIdTokenClaims(user, client); JwtClaims claims = createIdTokenClaims(user, client, atHash);
// 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.
@ -133,7 +142,7 @@ public class TokenController extends PathHandler {
jws.setPayload(claims.toJson()); jws.setPayload(claims.toJson());
jws.setKey(key.getPrivateKey()); jws.setKey(key.getPrivateKey());
jws.setKeyIdHeaderValue(key.getKeyId()); jws.setKeyIdHeaderValue(key.getKeyId());
jws.setAlgorithmHeaderValue(key.getAlgorithm()); jws.setAlgorithmHeaderValue(algo);
return jws.getCompactSerialization(); return jws.getCompactSerialization();
} catch (JoseException | KeyManager.KeyCreationException | IOException e) { } catch (JoseException | KeyManager.KeyCreationException | IOException e) {
@ -141,7 +150,24 @@ public class TokenController extends PathHandler {
} }
} }
private JwtClaims createIdTokenClaims(User user, Client client) { private String atHash(String algo, AccessToken accessToken) {
algo = "SHA" + algo.replaceAll("[^0-9]", "");
try {
var digest = MessageDigest.getInstance(algo);
byte[] hash = digest.digest(accessToken.id().getBytes(US_ASCII));
if (hash.length < 16) throw new RuntimeException("invalid hash (less than 128 bits)");
if (hash.length > 16) {
var trimmed = new byte[16];
for (var i = 0; i < 16; i++) trimmed[i] = hash[i];
hash = trimmed;
}
return BASE64.withoutPadding().encodeToString(hash); // https://stackoverflow.com/a/30356461
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private JwtClaims createIdTokenClaims(User user, Client client, String atHash) {
JwtClaims claims = new JwtClaims(); JwtClaims claims = new JwtClaims();
// required claims: // required claims:
@ -149,10 +175,10 @@ public class TokenController extends PathHandler {
claims.setSubject(user.uuid()); // the subject/principal is whom the token is about claims.setSubject(user.uuid()); // the subject/principal is whom the token is about
claims.setAudience(client.id()); claims.setAudience(client.id());
claims.setExpirationTimeMinutesInTheFuture(config.tokenExpirationMinutes); // time when the token will expire (10 minutes from now) claims.setExpirationTimeMinutesInTheFuture(config.tokenExpirationMinutes); // time when the token will expire (10 minutes from now)
claims.setIssuedAtToNow(); // when the token was issued/created (now) claims.setIssuedAtToNow();
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)); client.nonce().ifPresent(nonce -> claims.setClaim(NONCE, nonce));
claims.setGeneratedJwtId(); // a unique identifier for the token claims.setGeneratedJwtId(); // a unique identifier for the token
return claims; return claims;

1
de.srsoftware.oidc.web/src/main/resources/en/navigation.html

@ -3,4 +3,5 @@
<a href="users.html" class="MANAGE_USERS">Users</a> <a href="users.html" class="MANAGE_USERS">Users</a>
<a href="settings.html">Settings</a> <a href="settings.html">Settings</a>
<a href="todo.html">TODO</a> <a href="todo.html">TODO</a>
<a href="https://openid.net/specs/openid-connect-core-1_0.html" target="_blank">Spec</a>
<a href="logout.html">Logout</a> <a href="logout.html">Logout</a>

1
de.srsoftware.oidc.web/src/main/resources/en/todo.html

@ -12,7 +12,6 @@
<div id="content"> <div id="content">
<h1>to do…</h1> <h1>to do…</h1>
<ul> <ul>
<li>Separates Email-Konto</li>
<li>at_hash in ID Token</li> <li>at_hash in ID Token</li>
<li>Session bei Aktivität verlängern</li> <li>Session bei Aktivität verlängern</li>
<li>implement token refresh</li> <li>implement token refresh</li>

Loading…
Cancel
Save