io.camunda.identity.sdk.authentication.AbstractAuthentication Maven / Gradle / Ivy
The newest version!
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
* one or more contributor license agreements. Licensed under a proprietary license. See the
* License.txt file for more information. You may not use this file except in compliance with the
* proprietary license.
*/
package io.camunda.identity.sdk.authentication;
import static io.camunda.identity.sdk.utility.UrlUtility.combinePaths;
import static org.apache.commons.lang3.StringUtils.isNoneBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import com.auth0.jwk.InvalidPublicKeyException;
import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import io.camunda.identity.sdk.IdentityConfiguration;
import io.camunda.identity.sdk.authentication.exception.InvalidClaimException;
import io.camunda.identity.sdk.authentication.exception.InvalidSignatureException;
import io.camunda.identity.sdk.authentication.exception.JsonWebKeyException;
import io.camunda.identity.sdk.authentication.exception.TokenDecodeException;
import io.camunda.identity.sdk.authentication.exception.TokenExpiredException;
import io.camunda.identity.sdk.authentication.exception.TokenVerificationException;
import io.camunda.identity.sdk.cache.ClientTokenCache;
import io.camunda.identity.sdk.exception.IdentityException;
import io.camunda.identity.sdk.impl.GenericSingleSignOutBuilder;
import io.camunda.identity.sdk.impl.dto.WellKnownConfiguration;
import io.camunda.identity.sdk.impl.rest.RestClient;
import io.camunda.identity.sdk.impl.rest.request.GroupRequest;
import java.net.URI;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.apache.commons.lang3.NotImplementedException;
import org.ehcache.Cache;
/**
* The Authentication class provides functionality to authenticate a user with Identity
* and verify access tokens.
*/
public abstract class AbstractAuthentication implements Authentication {
public static final long JWKS_CACHE_SIZE = 5L;
public static final long JWKS_CACHE_LIFETIME_DAYS = 7L;
public static final String WELL_KNOWN_PATH = "/.well-known/openid-configuration";
static final String GROUPS_PATH = "/api/groups";
static final String FOR_TOKEN_PATH = "/for-token";
protected final IdentityConfiguration configuration;
protected final Cache tokenCache = new ClientTokenCache().getCache();
protected final RestClient restClient;
protected AbstractAuthentication(final IdentityConfiguration configuration,
final RestClient restClient) {
this.configuration = configuration;
this.restClient = restClient;
}
@Override
public boolean isAvailable() {
return isNoneBlank(
configuration.getIssuer(),
configuration.getIssuerBackendUrl(),
configuration.getClientId(),
configuration.getClientSecret()
);
}
/**
* Requests a client token from the cache if available. If
* no token is found with the required audience, a new token
* will be requested from the authentication provider and stored.
*
* @param audience the audience of the resource server
* @return the tokens
* @throws IdentityException if case of a failure
*/
@Override
public Tokens requestToken(final String audience) {
if (!tokenCache.containsKey(audience)) {
tokenCache.put(audience, requestFreshToken(audience));
}
return tokenCache.get(audience);
}
/**
* Decodes a token. Can be used to access tokens data without validation
*
* @param token token in JWT format
* @return decoded token
* @throws TokenDecodeException the token can not be decoded
*/
@Override
public DecodedJWT decodeJWT(final String token) {
try {
return JWT.decode(token);
} catch (final com.auth0.jwt.exceptions.JWTDecodeException e) {
throw new TokenDecodeException(e);
}
}
@Override
public AccessToken verifyTokenIgnoringAudience(final String token) {
return verifyToken(token, null, null);
}
/**
* Logs out from Identity backend based on the configuration, one of these cases is possible:
* 1. The refresh token is not empty and OAuth Provider (OP) has provided a revoke endpoint,
* then see {@link #revokeToken}, in this case the method return an empty Optional.
* 2. end-session endpoint is available, so in this case the url for logout is returned and
* client should handle the redirection
* 3. otherwise this method will throw exception
*
* @param refreshToken refresh token used for the request
* @param callbackUrl the URL to redirect to post-SSO if supported by SSO provider
* @throws IdentityException if token revocation has failed or neither revoke
* @throws NotImplementedException if case 3 happens
*/
@Override
public Optional singleSignOut(final String refreshToken, final String callbackUrl) {
if (isRevokeAvailable() && isNotBlank(refreshToken)) {
revokeToken(refreshToken);
return Optional.empty();
} else if (isSingleSignOutAvailable()) {
return Optional.of(generateSingleSignOutUri(callbackUrl));
}
throw new NotImplementedException(
"single sign out is not implemented for this case");
}
/**
* Verifies the validity of the passed token. Following checks will be performed:
*
* - The token is correctly signed
* - The token has not expired
* - Token's audience (aud claim) matches application's audience
*
*
* @param token the token
* @return the decoded jwt
* @throws TokenDecodeException the token can not be decoded
* @throws InvalidSignatureException the token's signature is invalid
* @throws TokenExpiredException the token has expired
* @throws InvalidClaimException the provided claim is invalid
* @throws JsonWebKeyException the JWK needed to verify token's signature can not be
* retrieved
*/
@Override
public AccessToken verifyToken(final String token) {
return verifyToken(token, configuration.getAudience(), null);
}
/**
* Verifies the validity of the passed token and organisation. Following checks will be performed:
*
* - The token is correctly signed
* - The token has not expired
* - Token's audience (aud claim) matches application's audience
*
*
* @param token the token
* @param organizationId the organisation of the token
* @return the decoded jwt
* @throws TokenDecodeException the token can not be decoded
* @throws InvalidSignatureException the token's signature is invalid
* @throws TokenExpiredException the token has expired
* @throws InvalidClaimException the provided claim is invalid
* @throws JsonWebKeyException the JWK needed to verify token's signature can not be
* retrieved
*/
@Override
public AccessToken verifyToken(final String token, final String organizationId) {
return verifyToken(token, configuration.getAudience(), organizationId);
}
/**
* Verifies the validity of the passed token. Following checks will be performed:
*
* - The token is correctly signed
* - The token has not expired
* - Token's audience (aud claim) matches provided audience
*
*
* @param token the token
* @param audience token's aud claim must match provided audience
* @return the decoded jwt
* @throws TokenDecodeException the token can not be decoded
* @throws InvalidSignatureException the token's signature is invalid
* @throws TokenExpiredException the token has expired
* @throws InvalidClaimException the provided claim is invalid
* @throws JsonWebKeyException the JWK needed to verify token's signature can not be
* retrieved
*/
protected AccessToken verifyToken(final String token,
final String audience,
final String organizationId) {
try {
final DecodedJWT jwt = verifyAndDecode(token, audience);
return new AccessToken(
jwt,
getPermissions(jwt, audience),
getAssignedOrganizations(jwt),
getUserDetails(jwt, organizationId));
} catch (final SignatureVerificationException | AlgorithmMismatchException e) {
throw new InvalidSignatureException(e);
} catch (final com.auth0.jwt.exceptions.TokenExpiredException e) {
throw new TokenExpiredException(e);
} catch (final com.auth0.jwt.exceptions.InvalidClaimException e) {
throw new InvalidClaimException(e);
}
}
protected SingleSignOutUriBuilder singleSignOutUriBuilder() {
return new GenericSingleSignOutBuilder(wellKnownConfiguration().getEndSessionEndpoint());
}
protected URI generateSingleSignOutUri(final String callbackUrl) {
return singleSignOutUriBuilder().build();
}
protected UserDetails getUserDetails(final DecodedJWT token, final String organizationId) {
return new UserDetails(
token.getSubject(),
token.getClaim("email").asString(),
token.getClaim("preferred_username").asString(),
token.getClaim("name").asString(),
getGroupsInOrganization(token, organizationId)
);
}
@Override
public DecodedJWT verifyAndDecode(final String token, final String audience) {
return verify(decodeJWT(token), audience);
}
private DecodedJWT verify(final DecodedJWT token, final String audience) {
try {
final Jwk jwk = jwkProvider().get(token.getKeyId());
verifyJwk(token, jwk);
final Algorithm algorithm = signatureValidationAlgorithm(jwk, token);
JWTVerifier tokenVerifier;
if (audience != null) {
tokenVerifier = JWT.require(algorithm).withAudience(audience).build();
} else {
tokenVerifier = JWT.require(algorithm).build();
}
return tokenVerifier.verify(token);
} catch (final JwkException e) {
throw new JsonWebKeyException("JWKS error", e);
}
}
private void verifyJwk(final DecodedJWT token, final Jwk jwk) {
if (jwk.getUsage() != null && !jwk.getUsage().equals("sig")) {
throw new TokenVerificationException(
"Token is signed with a JWK, that can not be used for signing");
}
if (jwk.getAlgorithm() != null && !jwk.getAlgorithm().equals(token.getAlgorithm())) {
throw new TokenVerificationException("JWT algorithm does not match JWK algorithm");
}
}
private Algorithm signatureValidationAlgorithm(final Jwk jwk, final DecodedJWT token)
throws InvalidPublicKeyException {
final var algorithm = getAlgorithm(jwk, token);
if (algorithm == null) {
return Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
}
switch (algorithm) {
case "RS256":
return Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
case "RS384":
return Algorithm.RSA384((RSAPublicKey) jwk.getPublicKey(), null);
case "RS512":
return Algorithm.RSA512((RSAPublicKey) jwk.getPublicKey(), null);
case "ES256":
return Algorithm.ECDSA256((ECPublicKey) jwk.getPublicKey(), null);
case "ES384":
return Algorithm.ECDSA384((ECPublicKey) jwk.getPublicKey(), null);
case "ES512":
return Algorithm.ECDSA512((ECPublicKey) jwk.getPublicKey(), null);
default:
throw new TokenVerificationException(
String.format("Signing algorithm '%s' is not supported", jwk.getAlgorithm()));
}
}
private String getAlgorithm(final Jwk jwk, final DecodedJWT token) {
return jwk.getAlgorithm() != null ? jwk.getAlgorithm() : token.getAlgorithm();
}
@Override
public List getPermissions(final String token) {
return getPermissions(token, configuration.getAudience());
}
@Override
public List getPermissions(final String token, final String audience) {
try {
final DecodedJWT jwt = verifyAndDecode(token, audience);
return getPermissions(jwt, audience);
} catch (final SignatureVerificationException | AlgorithmMismatchException e) {
throw new InvalidSignatureException(e);
} catch (final com.auth0.jwt.exceptions.TokenExpiredException e) {
throw new TokenExpiredException(e);
} catch (final com.auth0.jwt.exceptions.InvalidClaimException e) {
throw new InvalidClaimException(e);
}
}
protected abstract List getPermissions(final DecodedJWT token, final String audience);
@Override
public List getGroups(final String token) {
return getGroups(token, configuration.getAudience());
}
@Override
public List getGroups(final String token, final String audience) {
return getGroupsInOrganization(token, audience, null);
}
@Override
public List getGroupsInOrganization(final String token, final String organization) {
return getGroupsInOrganization(token, configuration.getAudience(), organization);
}
@Override
public List getGroupsInOrganization(final String token, final String audience,
final String organization) {
try {
final DecodedJWT jwt = verifyAndDecode(token, audience);
return getGroupsInOrganization(jwt, organization);
} catch (final SignatureVerificationException | AlgorithmMismatchException e) {
throw new InvalidSignatureException(e);
} catch (final com.auth0.jwt.exceptions.TokenExpiredException e) {
throw new TokenExpiredException(e);
} catch (final com.auth0.jwt.exceptions.InvalidClaimException e) {
throw new InvalidClaimException(e);
}
}
protected List getGroupsInOrganization(final DecodedJWT token,
final String organizationId) {
if (isNotBlank(configuration.getBaseUrl())) {
return restClient.request(
new GroupRequest(
combinePaths(configuration.getBaseUrl(), GROUPS_PATH, FOR_TOKEN_PATH),
token.getToken(),
organizationId
)
);
}
return Collections.emptyList();
}
protected abstract JwkProvider jwkProvider();
protected abstract WellKnownConfiguration wellKnownConfiguration();
protected abstract Tokens requestFreshToken(final String audience);
protected abstract boolean isRevokeAvailable();
protected abstract boolean isSingleSignOutAvailable();
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy