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

io.camunda.identity.sdk.authentication.AbstractAuthentication Maven / Gradle / Ivy

There is a newer version: 8.5.9
Show 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