All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tngtech.keycloakmock.api.TokenConfig Maven / Gradle / Ivy

The newest version!
package com.tngtech.keycloakmock.api;

import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * The configuration from which to generate an access token.
 *
 * 

Example usage: * *

{@code
 * TokenConfig config = TokenConfig.aTokenConfig().withSubjectAndGeneratedUserData("subject").build();
 * }
*/ public class TokenConfig { private static final Pattern ISSUER_PATH_PATTERN = Pattern.compile("^.*?/realms/([^/]+)$"); @Nonnull private final Set audience; @Nonnull private final String authorizedParty; @Nonnull private final String subject; @Nonnull private final List scopes; @Nonnull private final Map claims; @Nonnull private final Access realmAccess; @Nonnull private final Map resourceAccess; @Nonnull private final String sessionId; @Nonnull private final Instant issuedAt; @Nonnull private final Instant authenticationTime; private final boolean generateUserDataFromSubject; @Nullable private final Instant notBefore; @Nullable private final Instant expiration; @Nullable private final String hostname; @Nullable private final String realm; @Nullable private final String name; @Nullable private final String givenName; @Nullable private final String familyName; @Nullable private final String email; @Nullable private final String preferredUsername; @Nullable private final String authenticationContextClassReference; private TokenConfig(@Nonnull final Builder builder) { if (builder.audience.isEmpty()) { audience = Collections.singleton("server"); } else { audience = builder.audience; } authorizedParty = builder.authorizedParty; subject = builder.subject; generateUserDataFromSubject = builder.generateUserDataFromSubject; scopes = builder.scopes; claims = builder.claims; realmAccess = builder.realmRoles; resourceAccess = builder.resourceAccess; sessionId = builder.sessionId; issuedAt = builder.issuedAt; authenticationTime = builder.authenticationTime; hostname = builder.hostname; realm = builder.realm; notBefore = builder.notBefore; expiration = builder.expiration; givenName = builder.givenName; familyName = builder.familyName; if (builder.name != null) { name = builder.name; } else if (givenName != null) { if (familyName != null) { name = givenName + " " + familyName; } else { name = givenName; } } else { name = familyName; } email = builder.email; preferredUsername = builder.preferredUsername; authenticationContextClassReference = builder.authenticationContextClassReference; } /** * Get a new builder. * * @return a token config builder */ @Nonnull public static Builder aTokenConfig() { return new Builder(); } @Nonnull public Set getAudience() { return Collections.unmodifiableSet(audience); } @Nonnull public String getAuthorizedParty() { return authorizedParty; } @Nonnull public String getSubject() { return subject; } public boolean isGenerateUserDataFromSubject() { return generateUserDataFromSubject; } @Nonnull public List getScopes() { return scopes; } @Nonnull public Map getClaims() { return Collections.unmodifiableMap(claims); } @Nonnull public Access getRealmAccess() { return realmAccess; } @Nonnull public Map getResourceAccess() { return Collections.unmodifiableMap(resourceAccess); } @Nonnull public String getSessionId() { return sessionId; } @Nonnull public Instant getIssuedAt() { return issuedAt; } @Nonnull public Instant getAuthenticationTime() { return authenticationTime; } @Nullable public Instant getNotBefore() { return notBefore; } @Nullable public Instant getExpiration() { return expiration; } @Nullable public String getHostname() { return hostname; } @Nullable public String getRealm() { return realm; } @Nullable public String getName() { return name; } @Nullable public String getGivenName() { return givenName; } @Nullable public String getFamilyName() { return familyName; } @Nullable public String getEmail() { return email; } @Nullable public String getPreferredUsername() { return preferredUsername; } @Nullable public String getAuthenticationContextClassReference() { return authenticationContextClassReference; } /** * Builder for {@link TokenConfig}. * *

Use this to generate a token configuration to your needs. */ public static final class Builder { @Nonnull private final Set audience = new HashSet<>(); @Nonnull private String authorizedParty = "client"; @Nonnull private String subject = "user"; @Nonnull private final List scopes = new ArrayList<>(); @Nonnull private final Map claims = new HashMap<>(); @Nonnull private final Access realmRoles = new Access(); @Nonnull private final Map resourceAccess = new HashMap<>(); @Nonnull private String sessionId = UUID.randomUUID().toString(); @Nonnull private Instant issuedAt = Instant.now(); @Nonnull private Instant authenticationTime = Instant.now(); private boolean generateUserDataFromSubject = false; @Nullable private Instant notBefore; @Nullable private Instant expiration; @Nullable private String hostname; @Nullable private String realm; @Nullable private String givenName; @Nullable private String familyName; @Nullable private String name; @Nullable private String email; @Nullable private String preferredUsername; @Nullable private String authenticationContextClassReference; private Builder() {} /** * Add the contents of a real token. * *

The content of the token is read into this token config. Signature, issuer as well as * start and end time of the original token are ignored. * * @param originalToken token to read from * @return builder */ @Nonnull @SuppressWarnings("unchecked") public Builder withSourceToken(@Nonnull final String originalToken) { String[] split = originalToken.split("\\.", 3); String noneHeader = Base64.getUrlEncoder() .encodeToString("{\"alg\":\"NONE\"}".getBytes(StandardCharsets.UTF_8)); String untrustedJwtString = noneHeader + "." + split[1] + "."; Claims untrustedClaims; try { untrustedClaims = Jwts.parser().unsecured().build().parseUnsecuredClaims(untrustedJwtString).getPayload(); } catch (ClaimJwtException e) { // ignoring expiry exceptions untrustedClaims = e.getClaims(); } for (Map.Entry entry : untrustedClaims.entrySet()) { switch (entry.getKey()) { case "aud": Object aud = entry.getValue(); if (aud instanceof String) { withAudience((String) aud); } else if (aud instanceof Collection) { withAudiences((Collection) aud); } break; case "azp": withAuthorizedParty(getTypedValue(entry, String.class)); break; case "sub": withSubject(getTypedValue(entry, String.class)); break; case "name": withName(getTypedValue(entry, String.class)); break; case "given_name": withGivenName(getTypedValue(entry, String.class)); break; case "family_name": withFamilyName(getTypedValue(entry, String.class)); break; case "email": withEmail(getTypedValue(entry, String.class)); break; case "preferred_username": withPreferredUsername(getTypedValue(entry, String.class)); break; case "realm_access": Map> sourceRealmAccess = getTypedValue(entry, Map.class); withRealmRoles(sourceRealmAccess.get("roles")); break; case "resource_access": Map>> sourceResourceAccess = getTypedValue(entry, Map.class); sourceResourceAccess.forEach( (key, value) -> withResourceRoles(key, value.get("roles"))); break; case "scope": withScopes(Arrays.asList(getTypedValue(entry, String.class).split(" "))); break; case "acr": withAuthenticationContextClassReference(getTypedValue(entry, String.class)); break; case "typ": if (!"Bearer".equals(getTypedValue(entry, String.class))) { throw new IllegalArgumentException("Only bearer tokens are allowed here!"); } break; case "iss": String issuer = getTypedValue(entry, String.class); try { URI issuerUrl = new URI(issuer); withHostname(issuerUrl.getHost()); withRealm(getRealm(issuerUrl)); } catch (URISyntaxException e) { throw new IllegalArgumentException("Issuer '" + issuer + "' is not a valid URL", e); } break; case "sid": case "session_state": // ignoring session information break; case "iat": case "nbf": case "exp": case "auth_time": // ignoring date information break; default: withClaim(entry.getKey(), entry.getValue()); break; } } return this; } @SuppressWarnings("unchecked") @Nonnull private T getTypedValue( @Nonnull final Map.Entry entry, @Nonnull final Class clazz) { if (clazz.isInstance(entry.getValue())) { return (T) entry.getValue(); } throw new IllegalArgumentException( String.format( "Expected %s for key %s, but found %s", clazz, entry.getKey(), entry.getValue().getClass())); } @Nonnull private String getRealm(@Nonnull final URI issuer) { Matcher matcher = ISSUER_PATH_PATTERN.matcher(issuer.getPath()); if (!matcher.matches()) { throw new IllegalArgumentException( "The issuer '" + issuer + "' did not conform to the expected format" + " 'http[s]://$HOSTNAME[:$PORT][/$CONTEXT_PATH]/realms/$REALM'."); } return matcher.group(1); } /** * Add an audience. * *

An audience is an identifier of a recipient of the token. * * @param audience the audience to add * @return builder * @see ID token */ @Nonnull public Builder withAudience(@Nonnull final String audience) { this.audience.add(Objects.requireNonNull(audience)); return this; } /** * Add a collection of audiences. * *

An audience is an identifier of a recipient of the token. * * @param audiences the audiences to add * @return builder * @see ID token */ @Nonnull public Builder withAudiences(@Nonnull final Collection audiences) { this.audience.addAll(Objects.requireNonNull(audiences)); return this; } /** * Set authorized party. * *

The authorized party identifies the party for which the token was issued. * * @param authorizedParty the authorized party to set * @return builder * @see ID token */ @Nonnull public Builder withAuthorizedParty(@Nonnull final String authorizedParty) { this.authorizedParty = Objects.requireNonNull(authorizedParty); return this; } /** * Set subject. * *

The subject identifies the end-user for which the token was issued. * * @param subject the subject to set * @return builder * @see ID token * @see #withSubjectAndGeneratedUserData(String) to also generate name, preferred username and * email. */ @Nonnull public Builder withSubject(@Nonnull final String subject) { this.subject = Objects.requireNonNull(subject); return this; } /** * Set subject and derive name, preferred username and email. * *

Note that values explicitly set via {@link #withName(String)}, {@link * #withGivenName(String)}, {@link #withFamilyName(String)}, {@link * #withPreferredUsername(String)} or {@link #withEmail(String)} take preference even when using * this method. * * @param subject the subject to set * @return builder * @see ID token * @see #withSubject(String) to only set the subject */ @Nonnull public Builder withSubjectAndGeneratedUserData(@Nonnull final String subject) { this.subject = Objects.requireNonNull(subject); this.generateUserDataFromSubject = true; return this; } /** * Add scope. * *

The scope for which this token has been requested. If not set, the default scopes * configured in {@link ServerConfig} will be used. * * @param scope the scope to add * @return builder * @see scope * claims */ @Nonnull public Builder withScope(@Nonnull final String scope) { this.scopes.add(scope); return this; } /** * Add scopes. * *

The scopes for which this token has been requested. If not set, the default scopes * configured in {@link ServerConfig} will be used. * * @param scopes the scopes to add * @return builder * @see scope * claims */ @Nonnull public Builder withScopes(@Nonnull final Collection scopes) { this.scopes.addAll(scopes); return this; } /** * Add authorization hostname. * *

The hostname for which this token has been requested. If not set, the default hostname of * the mock is used. * *

This will be used to construct the issuer "iss": http[s]://$HOSTNAME/auth/realms/$REALM. * The protocol is taken from the server configuration. * *

Note: The hostname must not contain a protocol prefix like 'http://'. * * @param hostname the hostname * @return builder * @see #withRealm(String) * @see ServerConfig#getProtocol() */ @Nonnull public Builder withHostname(@Nonnull final String hostname) { this.hostname = Objects.requireNonNull(hostname); return this; } /** * Add authorization realm. * *

The realm for which this token has been requested. If not set, the default realm of the * mock is used. * *

This will be used to construct the issuer "iss": http[s]://$HOSTNAME/auth/realms/$REALM. * The protocol is taken from the server configuration. * * @param realm the realm * @return builder * @see #withHostname(String) * @see ServerConfig#getProtocol() */ @Nonnull public Builder withRealm(@Nonnull final String realm) { this.realm = Objects.requireNonNull(realm); return this; } /** * Add realm roles. * *

Realm roles apply to all clients within a realm. * * @param roles the roles to add * @return builder * @see realm * roles */ @Nonnull public Builder withRealmRoles(@Nonnull final Collection roles) { this.realmRoles.addRoles(Objects.requireNonNull(roles)); return this; } /** * Add a realm role. * *

Realm roles apply to all clients within a realm. * * @param role the role to add * @return builder * @see realm * roles */ @Nonnull public Builder withRealmRole(@Nonnull final String role) { this.realmRoles.addRole(Objects.requireNonNull(role)); return this; } /** * Add resource roles. * *

Resource roles only apply to a specific client or resource. * * @param resource the resource or client for which to add the roles * @param roles the roles to add * @return builder * @see client * roles */ @Nonnull public Builder withResourceRoles( @Nonnull final String resource, @Nonnull final Collection roles) { this.resourceAccess .computeIfAbsent(Objects.requireNonNull(resource), k -> new Access()) .addRoles(Objects.requireNonNull(roles)); return this; } /** * Add a resource role. * *

Resource roles only apply to a specific client or resource. * * @param resource the resource or client for which to add the roles * @param role the role to add * @return builder * @see client * roles */ @Nonnull public Builder withResourceRole(@Nonnull final String resource, @Nonnull final String role) { this.resourceAccess .computeIfAbsent(Objects.requireNonNull(resource), k -> new Access()) .addRole(Objects.requireNonNull(role)); return this; } /** * Add a session ID. * *

Claim to identify the session ID of the current login session to be used for back-channel * logout. * * @param sessionId the session ID of the current login session. * @return builder * @see Indicating * OP Support for Back-Channel Logout */ @Nonnull public Builder withSessionId(@Nonnull final String sessionId) { this.sessionId = Objects.requireNonNull(sessionId); return this; } /** * Add generic claims. * *

Use this method to add elements to the token cannot be set using more specialized methods. * The underlying library uses Jackson * data-binding, so getters will be used to generate JSON objects. * * @param claims the claims to add (map from claim name to claim value) * @return builder */ @Nonnull public Builder withClaims(@Nonnull final Map claims) { this.claims.putAll(Objects.requireNonNull(claims)); return this; } /** * Add generic claim. * *

Use this method to add elements to the token cannot be set using more specialized methods. * The underlying library uses Jackson * data-binding, so getters will be used to generate JSON objects. * * @param key the claim name * @param value the claim value * @return builder */ @Nonnull public Builder withClaim(@Nonnull final String key, @Nonnull final Object value) { this.claims.put(Objects.requireNonNull(key), Objects.requireNonNull(value)); return this; } /** * Set issued at date. * * @param issuedAt the instant when the token was generated * @return builder * @see ID token */ @Nonnull public Builder withIssuedAt(@Nonnull final Instant issuedAt) { this.issuedAt = Objects.requireNonNull(issuedAt); return this; } /** * Set authentication time. * * @param authenticationTime the instant when user was authenticated at the SSO server * @return builder * @see ID token */ @Nonnull public Builder withAuthenticationTime(@Nonnull final Instant authenticationTime) { this.authenticationTime = Objects.requireNonNull(authenticationTime); return this; } /** * Set expiration date. * *

As an alternative, you can also set the token lifespan instead using {@link * #withTokenLifespan(Duration)}. * *

If no expiration is configured, the default token lifespan configured in {@link * ServerConfig.Builder#withDefaultTokenLifespan(Duration)} will be used. * * @param expiration the instant when the token expires * @return builder * @see ID token * @see #withTokenLifespan(Duration) */ @Nonnull public Builder withExpiration(@Nonnull final Instant expiration) { this.expiration = Objects.requireNonNull(expiration); return this; } /** * Set lifespan of generated token. * *

This is an alternative option to setting the expiration directly via {@link * #withExpiration(Instant)}. The expiration will be calculated as issuedAt + tokenLifespan. * *

If no token lifespan is configured, the default token lifespan configured in {@link * ServerConfig.Builder#withDefaultTokenLifespan(Duration)} will be used. * * @param tokenLifespan duration the token should be valid * @return builder * @see #withExpiration(Instant) */ public Builder withTokenLifespan(@Nonnull final Duration tokenLifespan) { this.expiration = issuedAt.plus(tokenLifespan); return this; } /** * Set not before date. * * @param notBefore the instant when the token starts being valid * @return builder * @see Not Before Claim */ @Nonnull public Builder withNotBefore(@Nullable final Instant notBefore) { this.notBefore = notBefore; return this; } /** * Set given name. * * @param givenName the given name of the user * @return builder * @see create * new user */ @Nonnull public Builder withGivenName(@Nullable final String givenName) { this.givenName = givenName; return this; } /** * Set family name. * * @param familyName the family name of the user * @return builder * @see create * new user */ @Nonnull public Builder withFamilyName(@Nullable final String familyName) { this.familyName = familyName; return this; } /** * Set full name. * *

If not set, this will automatically be filled using given name and family name. * * @param name the full name of the user * @return builder */ @Nonnull public Builder withName(@Nullable final String name) { this.name = name; return this; } /** * Set email address. * * @param email the email address of the user * @return builder * @see create * new user */ @Nonnull public Builder withEmail(@Nullable final String email) { this.email = email; return this; } /** * Set preferred username. * * @param preferredUsername the preferred username of the user * @return builder * @see create * new user */ @Nonnull public Builder withPreferredUsername(@Nullable final String preferredUsername) { this.preferredUsername = preferredUsername; return this; } /** * Set authentication context class reference. * * @param authenticationContextClassReference the reference class of the authentication ("0" or * "1") * @return builder * @see ID token */ @Nonnull public Builder withAuthenticationContextClassReference( @Nullable final String authenticationContextClassReference) { this.authenticationContextClassReference = authenticationContextClassReference; return this; } @Nonnull public TokenConfig build() { return new TokenConfig(this); } } /** * A container for realm or resource roles, to be used in claims {@code realm_access} or {@code * resource_access}. * * @see Builder#withRealmRole(String) * @see Builder#withResourceRole(String, String) */ public static class Access { @Nonnull private final Set roles = new HashSet<>(); @Nonnull public Set getRoles() { return Collections.unmodifiableSet(roles); } void addRole(@Nonnull final String role) { roles.add(Objects.requireNonNull(role)); } void addRoles(@Nonnull final Collection newRoles) { roles.addAll(newRoles); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy