com.tngtech.keycloakmock.api.TokenConfig Maven / Gradle / Ivy
Show all versions of mock Show documentation
package com.tngtech.keycloakmock.api;
import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
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 javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* The configuration from which to generate an access token.
*
* Example usage:
*
*
{@code
* TokenConfig config = TokenConfig.aTokenConfig().withSubject("subject).build();
* }
*/
public class TokenConfig {
@Nonnull private final Set audience;
@Nonnull private final String authorizedParty;
@Nonnull private final String subject;
@Nonnull private final String scope;
@Nonnull private final Map claims;
@Nonnull private final Access realmAccess;
@Nonnull private final Map resourceAccess;
@Nonnull private final Instant issuedAt;
@Nonnull private final Instant authenticationTime;
@Nonnull private final Instant expiration;
@Nullable private final Instant notBefore;
@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;
private TokenConfig(@Nonnull final Builder builder) {
if (builder.audience.isEmpty()) {
audience = Collections.singleton("server");
} else {
audience = builder.audience;
}
authorizedParty = builder.authorizedParty;
subject = builder.subject;
scope = String.join(" ", builder.scope);
claims = builder.claims;
realmAccess = builder.realmRoles;
resourceAccess = builder.resourceAccess;
issuedAt = builder.issuedAt;
authenticationTime = builder.authenticationTime;
expiration = builder.expiration;
notBefore = builder.notBefore;
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;
}
/**
* 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;
}
@Nonnull
public String getScope() {
return scope;
}
@Nonnull
public Map getClaims() {
return Collections.unmodifiableMap(claims);
}
@Nonnull
public Access getRealmAccess() {
return realmAccess;
}
@Nonnull
public Map getResourceAccess() {
return Collections.unmodifiableMap(resourceAccess);
}
@Nonnull
public Instant getIssuedAt() {
return issuedAt;
}
@Nonnull
public Instant getAuthenticationTime() {
return authenticationTime;
}
@Nonnull
public Instant getExpiration() {
return expiration;
}
@Nullable
public Instant getNotBefore() {
return notBefore;
}
@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;
}
/**
* 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 Set scope = new HashSet<>();
@Nonnull private final Map claims = new HashMap<>();
@Nonnull private final Access realmRoles = new Access();
@Nonnull private final Map resourceAccess = new HashMap<>();
@Nonnull private Instant issuedAt = Instant.now();
@Nonnull private Instant expiration = issuedAt.plus(10, ChronoUnit.HOURS);
@Nonnull private Instant authenticationTime = Instant.now();
@Nullable private Instant notBefore;
@Nullable private String givenName;
@Nullable private String familyName;
@Nullable private String name;
@Nullable private String email;
@Nullable private String preferredUsername;
private Builder() {
scope.add("openid");
}
/**
* 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) {
int i = originalToken.lastIndexOf('.');
String untrustedJwtString = originalToken.substring(0, i + 1);
Claims untrustedClaims;
try {
untrustedClaims = Jwts.parserBuilder().build().parseClaimsJwt(untrustedJwtString).getBody();
} 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 "typ":
if (!"Bearer".equals(getTypedValue(entry, String.class))) {
throw new IllegalArgumentException("Only bearer tokens are allowed here!");
}
break;
case "iss":
case "iat":
case "nbf":
case "exp":
case "auth_time":
// ignoring issuer and 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()));
}
/**
* 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
*/
@Nonnull
public Builder withSubject(@Nonnull final String subject) {
this.subject = Objects.requireNonNull(subject);
return this;
}
/**
* Add scope.
*
*
The scope for which this token has been requested. Always contains "openid".
*
* @param scope the scope to add
* @return builder
* @see scope
* claims
*/
@Nonnull
public Builder withScope(@Nonnull final String scope) {
this.scope.add(scope);
return this;
}
/**
* Add scopes.
*
*
The scope for which this token has been requested. Always contains "openid".
*
* @param scopes the scopes to add
* @return builder
* @see scope
* claims
*/
@Nonnull
public Builder withScopes(@Nonnull final Collection scopes) {
this.scope.addAll(scopes);
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 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.
*
* @param expiration the instant when the token expires
* @return builder
* @see ID token
*/
@Nonnull
public Builder withExpiration(@Nonnull final Instant expiration) {
this.expiration = Objects.requireNonNull(expiration);
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;
}
@Nonnull
public TokenConfig build() {
return new TokenConfig(this);
}
}
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);
}
}
}