<commit_before>/*
 * Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 * 
 * WSO2 Inc. licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.wso2.carbon.identity.openidconnect;

import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
import org.apache.axiom.om.OMElement;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.base.MultitenantConstants;
import org.wso2.carbon.identity.application.authentication.framework.AuthenticationMethodNameTranslator;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser;
import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException;
import org.wso2.carbon.identity.application.common.model.ClaimConfig;
import org.wso2.carbon.identity.application.common.model.ClaimMapping;
import org.wso2.carbon.identity.application.common.model.FederatedAuthenticatorConfig;
import org.wso2.carbon.identity.application.common.model.IdentityProvider;
import org.wso2.carbon.identity.application.common.model.ServiceProvider;
import org.wso2.carbon.identity.application.common.util.IdentityApplicationConstants;
import org.wso2.carbon.identity.application.common.util.IdentityApplicationManagementUtil;
import org.wso2.carbon.identity.application.mgt.ApplicationManagementService;
import org.wso2.carbon.identity.base.IdentityConstants;
import org.wso2.carbon.identity.base.IdentityException;
import org.wso2.carbon.identity.core.util.IdentityConfigParser;
import org.wso2.carbon.identity.core.util.IdentityCoreConstants;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCache;
import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCacheEntry;
import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCacheKey;
import org.wso2.carbon.identity.oauth.cache.CacheEntry;
import org.wso2.carbon.identity.oauth.cache.OIDCAudienceCache;
import org.wso2.carbon.identity.oauth.cache.OIDCAudienceCacheEntry;
import org.wso2.carbon.identity.oauth.cache.OIDCAudienceCacheKey;
import org.wso2.carbon.identity.oauth.common.OAuthConstants;
import org.wso2.carbon.identity.oauth.common.exception.InvalidOAuthClientException;
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
import org.wso2.carbon.identity.oauth.dao.OAuthAppDAO;
import org.wso2.carbon.identity.oauth.dao.OAuthAppDO;
import org.wso2.carbon.identity.oauth2.IDTokenValidationFailureException;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext;
import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenRespDTO;
import org.wso2.carbon.identity.oauth2.dto.OAuth2AuthorizeReqDTO;
import org.wso2.carbon.identity.oauth2.dto.OAuth2AuthorizeRespDTO;
import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder;
import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext;
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;
import org.wso2.carbon.identity.openidconnect.internal.OpenIDConnectServiceComponentHolder;
import org.wso2.carbon.idp.mgt.IdentityProviderManagementException;
import org.wso2.carbon.idp.mgt.IdentityProviderManager;
import org.wso2.carbon.user.core.UserStoreException;
import org.wso2.carbon.user.core.UserStoreManager;
import org.wso2.carbon.user.core.util.UserCoreUtil;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;

import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.OIDCClaims.AUTH_TIME;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.OIDCClaims.AZP;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.OIDCClaims.NONCE;

/**
 * Default IDToken generator for the OpenID Connect Implementation.
 * This IDToken Generator utilizes the Nimbus SDK to build the IDToken.
 */
public class DefaultIDTokenBuilder implements org.wso2.carbon.identity.openidconnect.IDTokenBuilder {

    private static final String SHA384 = "SHA-384";
    private static final String SHA512 = "SHA-512";
    private static final String AUTHORIZATION_CODE = "AuthorizationCode";
    private static final String INBOUND_AUTH2_TYPE = "oauth2";
    private static final String CONFIG_ELEM_OAUTH = "OAuth";
    private static final String OPENID_CONNECT = "OpenIDConnect";
    private static final String OPENID_CONNECT_AUDIENCES = "Audiences";
    private static final String OPENID_CONNECT_AUDIENCE = "Audience";
    private static final String OPENID_IDP_ENTITY_ID = "IdPEntityId";

    private static final Log log = LogFactory.getLog(DefaultIDTokenBuilder.class);
    private JWSAlgorithm signatureAlgorithm;
    private JWEAlgorithm encryptionAlgorithm;
    private EncryptionMethod encryptionMethod;

    public DefaultIDTokenBuilder() throws IdentityOAuth2Exception {
        // Map signature algorithm from identity.xml to nimbus format, this is a one time configuration.
        signatureAlgorithm = OAuth2Util.mapSignatureAlgorithmForJWSAlgorithm(
                OAuthServerConfiguration.getInstance().getIdTokenSignatureAlgorithm());
    }

    /**
     * Create an oAuthAppDO object using client id and set encryption algorithm and encryption method.
     * @param clientId  ID of the client.
     * @throws IdentityOAuth2Exception
     */
    private void setupEncryptionAlgorithms(OAuthAppDO oAuthAppDO, String clientId) throws IdentityOAuth2Exception {

        encryptionAlgorithm = OAuth2Util.mapEncryptionAlgorithmForJWEAlgorithm(
                    oAuthAppDO.getIdTokenEncryptionAlgorithm());
        encryptionMethod = OAuth2Util.mapEncryptionMethodForJWEAlgorithm(oAuthAppDO.getIdTokenEncryptionMethod());

        if (log.isDebugEnabled()) {
            log.debug("Id token encryption is enabled using encryption algorithm: " + encryptionAlgorithm +
                    " and encryption method: " + encryptionMethod + ", for client: " + clientId);
        }
    }

    @Override
    public String buildIDToken(OAuthTokenReqMessageContext tokenReqMsgCtxt,
                               OAuth2AccessTokenRespDTO tokenRespDTO) throws IdentityOAuth2Exception {
        String clientId = tokenReqMsgCtxt.getOauth2AccessTokenReqDTO().getClientId();
        String spTenantDomain = getSpTenantDomain(tokenReqMsgCtxt);
        String idTokenIssuer = getIdTokenIssuer(spTenantDomain);
        String accessToken = tokenRespDTO.getAccessToken();

        long idTokenValidityInMillis = getIDTokenExpiryInMillis();
        long currentTimeInMillis = Calendar.getInstance().getTimeInMillis();

        AuthenticatedUser authorizedUser = tokenReqMsgCtxt.getAuthorizedUser();
        String subjectClaim = getSubjectClaim(tokenReqMsgCtxt, tokenRespDTO, clientId, spTenantDomain, authorizedUser);

        String nonceValue = null;
        long authTime = 0;
        String acrValue = null;
        List<String> amrValues = Collections.emptyList();

        // AuthorizationCode only available for authorization code grant type
        if (getAuthorizationCode(tokenReqMsgCtxt) != null) {
            AuthorizationGrantCacheEntry authzGrantCacheEntry = getAuthorizationGrantCacheEntryFromToken
                    (tokenRespDTO.getAccessToken());
            if (authzGrantCacheEntry != null) {
                nonceValue = authzGrantCacheEntry.getNonceValue();
                acrValue = authzGrantCacheEntry.getSelectedAcrValue();
                if (isAuthTimeRequired(authzGrantCacheEntry)) {
                    authTime = authzGrantCacheEntry.getAuthTime();
                }
                amrValues = authzGrantCacheEntry.getAmrList();
            }
        } else {
            amrValues = tokenReqMsgCtxt.getOauth2AccessTokenReqDTO().getAuthenticationMethodReferences();
        }

        if (log.isDebugEnabled()) {
            log.debug(buildDebugMessage(idTokenIssuer, subjectClaim, nonceValue, idTokenValidityInMillis,
                    currentTimeInMillis));
        }

        List<String> audience = getOIDCAudience(clientId, spTenantDomain);

        JWTClaimsSet jwtClaimsSet = new JWTClaimsSet();
        jwtClaimsSet.setIssuer(idTokenIssuer);
        jwtClaimsSet.setAudience(audience);
        jwtClaimsSet.setClaim(AZP, clientId);
        jwtClaimsSet.setExpirationTime(getIdTokenExpiryInMillis(idTokenValidityInMillis, currentTimeInMillis));
        jwtClaimsSet.setIssueTime(new Date(currentTimeInMillis));
        if (authTime != 0) {
            jwtClaimsSet.setClaim(AUTH_TIME, authTime / 1000);
        }
        if (nonceValue != null) {
            jwtClaimsSet.setClaim(NONCE, nonceValue);
        }
        if (StringUtils.isNotEmpty(acrValue)) {
            jwtClaimsSet.setClaim(OAuthConstants.ACR, acrValue);
        }
        if (amrValues != null) {
            jwtClaimsSet.setClaim(OAuthConstants.AMR, translateAmrToResponse(amrValues));
        }

        setAdditionalClaims(tokenReqMsgCtxt, tokenRespDTO, jwtClaimsSet);

        tokenReqMsgCtxt.addProperty(OAuthConstants.ACCESS_TOKEN, accessToken);
        tokenReqMsgCtxt.addProperty(MultitenantConstants.TENANT_DOMAIN, getSpTenantDomain(tokenReqMsgCtxt));

        handleOIDCCustomClaims(tokenReqMsgCtxt, jwtClaimsSet);
        jwtClaimsSet.setSubject(subjectClaim);

        if (isInvalidToken(jwtClaimsSet)) {
            throw new IDTokenValidationFailureException("Error while validating ID Token token for required claims");
        }

        if (isUnsignedIDToken()) {
            return new PlainJWT(jwtClaimsSet).serialize();
        }

        // Initialize OAuthAppDO using the client ID.
        OAuthAppDO oAuthAppDO;
        try {
            oAuthAppDO = OAuth2Util.getAppInformationByClientId(clientId);
        } catch (InvalidOAuthClientException e) {
            String error = "Error occurred while getting app information for client_id: " + clientId;
            throw new IdentityOAuth2Exception(error, e);
        }

        return getIDToken(clientId, spTenantDomain, jwtClaimsSet, oAuthAppDO, getSigningTenantDomain(tokenReqMsgCtxt));
    }

    @Override
    public String buildIDToken(OAuthAuthzReqMessageContext authzReqMessageContext,
                               OAuth2AuthorizeRespDTO tokenRespDTO) throws IdentityOAuth2Exception {

        String accessToken = tokenRespDTO.getAccessToken();
        String clientId = authzReqMessageContext.getAuthorizationReqDTO().getConsumerKey();
        String spTenantDomain = getSpTenantDomain(authzReqMessageContext);
        String issuer = getIdTokenIssuer(spTenantDomain);

        // Get subject from Authenticated Subject Identifier
        AuthenticatedUser authorizedUser = authzReqMessageContext.getAuthorizationReqDTO().getUser();
        String subject =
                getSubjectClaim(authzReqMessageContext, tokenRespDTO, clientId, spTenantDomain, authorizedUser);

        String nonceValue = authzReqMessageContext.getAuthorizationReqDTO().getNonce();
        String acrValue = authzReqMessageContext.getAuthorizationReqDTO().getSelectedAcr();
        List<String> amrValues = Collections.emptyList(); //TODO:

        long idTokenLifeTimeInMillis = getIDTokenExpiryInMillis();
        long currentTimeInMillis = Calendar.getInstance().getTimeInMillis();

        if (log.isDebugEnabled()) {
            log.debug(buildDebugMessage(issuer, subject, nonceValue, idTokenLifeTimeInMillis, currentTimeInMillis));
        }

        JWTClaimsSet jwtClaimsSet = new JWTClaimsSet();
        jwtClaimsSet.setIssuer(issuer);

        // Set the audience
        List<String> audience = getOIDCAudience(clientId, spTenantDomain);
        jwtClaimsSet.setAudience(audience);

        jwtClaimsSet.setClaim(AZP, clientId);
        jwtClaimsSet.setExpirationTime(getIdTokenExpiryInMillis(idTokenLifeTimeInMillis, currentTimeInMillis));
        jwtClaimsSet.setIssueTime(new Date(currentTimeInMillis));

        long authTime = getAuthTime(authzReqMessageContext);
        if (authTime != 0) {
            jwtClaimsSet.setClaim(AUTH_TIME, authTime / 1000);
        }
        if (nonceValue != null) {
            jwtClaimsSet.setClaim(OAuthConstants.OIDCClaims.NONCE, nonceValue);
        }
        if (StringUtils.isNotEmpty(acrValue)) {
            jwtClaimsSet.setClaim("acr", acrValue);
        }
        if (amrValues != null) {
            jwtClaimsSet.setClaim("amr", translateAmrToResponse(amrValues));
        }

        setAdditionalClaims(authzReqMessageContext, tokenRespDTO, jwtClaimsSet);

        authzReqMessageContext.addProperty(OAuthConstants.ACCESS_TOKEN, accessToken);
        authzReqMessageContext
                .addProperty(MultitenantConstants.TENANT_DOMAIN, getSpTenantDomain(authzReqMessageContext));
        handleCustomOIDCClaims(authzReqMessageContext, jwtClaimsSet);
        jwtClaimsSet.setSubject(subject);

        if (isUnsignedIDToken()) {
            return new PlainJWT(jwtClaimsSet).serialize();
        }

        // Initialize OAuthAppDO using the client ID.
        OAuthAppDO oAuthAppDO;
        try {
            oAuthAppDO = OAuth2Util.getAppInformationByClientId(clientId);
        } catch (InvalidOAuthClientException e) {
            String error = "Error occurred while getting app information for client_id: " + clientId;
            throw new IdentityOAuth2Exception(error, e);
        }

        return getIDToken(clientId, spTenantDomain, jwtClaimsSet, oAuthAppDO,
                getSigningTenantDomain(authzReqMessageContext));
    }

    private String getIDToken(String clientId, String spTenantDomain, JWTClaimsSet jwtClaimsSet, OAuthAppDO oAuthAppDO,
                              String signingTenantDomain) throws IdentityOAuth2Exception {

        if (oAuthAppDO.isIdTokenEncryptionEnabled()) {
            setupEncryptionAlgorithms(oAuthAppDO, clientId);
            return OAuth2Util.encryptJWT(jwtClaimsSet, encryptionAlgorithm, encryptionMethod, spTenantDomain,
                    clientId).serialize();
        } else {
            return OAuth2Util.signJWT(jwtClaimsSet, signatureAlgorithm, signingTenantDomain).serialize();
        }
    }

    protected String getSubjectClaim(OAuthTokenReqMessageContext tokenReqMessageContext,
                                     OAuth2AccessTokenRespDTO tokenRespDTO,
                                     String clientId,
                                     String spTenantDomain,
                                     AuthenticatedUser authorizedUser) throws IdentityOAuth2Exception {
        String accessToken = tokenRespDTO.getAccessToken();
        String subjectClaim = OIDCClaimUtil.getSubjectClaimCachedAgainstAccessToken(accessToken);
        if (isNotBlank(subjectClaim)) {
            if (log.isDebugEnabled()) {
                if (IdentityUtil.isTokenLoggable(IdentityConstants.IdentityTokens.USER_CLAIMS)) {
                    log.debug("Subject claim cached against the access token found for user: " + authorizedUser);
                } else {
                    log.debug("Subject claim: " + subjectClaim + " cached against the access token found for user: " +
                            authorizedUser);
                }
            }
            return subjectClaim;
        }
        return getSubjectClaim(clientId, spTenantDomain, authorizedUser);
    }

    protected String getSubjectClaim(OAuthAuthzReqMessageContext authzReqMessageContext,
                                     OAuth2AuthorizeRespDTO authorizeRespDTO,
                                     String clientId,
                                     String spTenantDomain,
                                     AuthenticatedUser authorizedUser) throws IdentityOAuth2Exception {
        String accessToken = authorizeRespDTO.getAccessToken();
        String subjectClaim = OIDCClaimUtil.getSubjectClaimCachedAgainstAccessToken(accessToken);
        if (isNotBlank(subjectClaim)) {
            if (log.isDebugEnabled()) {
                if (IdentityUtil.isTokenLoggable(IdentityConstants.IdentityTokens.USER_CLAIMS)) {
                    log.debug("Subject claim cached against the authz code found for user: " + authorizedUser);
                } else {
                    log.debug("Subject claim: " + subjectClaim + " cached against the authz code found for user: " +
                            authorizedUser);
                }
            }
            return subjectClaim;
        }
        return getSubjectClaim(clientId, spTenantDomain, authorizedUser);
    }

    private String getSubjectClaim(String clientId,
                                   String spTenantDomain,
                                   AuthenticatedUser authorizedUser) throws IdentityOAuth2Exception {
        String subjectClaim;
        if (isLocalUser(authorizedUser)) {
            // If the user is local then we need to find the subject claim of the user defined in SP configs and
            // append userStoreDomain/tenantDomain as configured
            ServiceProvider serviceProvider = getServiceProvider(spTenantDomain, clientId);
            if (serviceProvider == null) {
                throw new IdentityOAuth2Exception("Cannot find an service provider for client_id: " + clientId + " " +
                        "in tenantDomain: " + spTenantDomain);
            }
            subjectClaim = getSubjectClaimForLocalUser(serviceProvider, authorizedUser);
            if (log.isDebugEnabled()) {
                log.debug("Subject claim: " + subjectClaim + " set for local user: " + authorizedUser + " for " +
                        "application: " + clientId + " of tenantDomain: " + spTenantDomain);
            }
        } else {
            subjectClaim = authorizedUser.getAuthenticatedSubjectIdentifier();
            if (log.isDebugEnabled()) {
                log.debug("Subject claim: " + subjectClaim + " set for federated user: " + authorizedUser + " for " +
                        "application: " + clientId + " of tenantDomain: " + spTenantDomain);
            }
        }
        return subjectClaim;
    }

    private String buildDebugMessage(String issuer, String subject, String nonceValue, long idTokenLifeTimeInMillis,
                                     long currentTimeInMillis) {
        return (new StringBuilder())
                .append("Using issuer ").append(issuer).append("\n")
                .append("Subject ").append(subject).append("\n")
                .append("ID Token life time ").append(idTokenLifeTimeInMillis / 1000).append("\n")
                .append("Current time ").append(currentTimeInMillis / 1000).append("\n")
                .append("Nonce Value ").append(nonceValue).append("\n")
                .append("Signature Algorithm ").append(signatureAlgorithm).append("\n")
                .toString();
    }

    private boolean isInvalidToken(JWTClaimsSet jwtClaimsSet) {
        return !isValidIdToken(jwtClaimsSet);
    }

    private boolean isEssentialClaim(AuthorizationGrantCacheEntry authorizationGrantCacheEntry, String oidcClaimUri) {
        return isEssentialClaim(authorizationGrantCacheEntry.getEssentialClaims(), oidcClaimUri);
    }

    private boolean isEssentialClaim(String essentialClaims, String oidcClaimUri) {
        return StringUtils.isNotBlank(essentialClaims) &&
                OAuth2Util.getEssentialClaims(essentialClaims, OAuthConstants.ID_TOKEN).contains(oidcClaimUri);
    }

    private boolean isMaxAgePresentInAuthzRequest(AuthorizationGrantCacheEntry authorizationGrantCacheEntry) {
        return authorizationGrantCacheEntry.getMaxAge() != 0;
    }

    private boolean isUnsignedIDToken() {
        return JWSAlgorithm.NONE.getName().equals(signatureAlgorithm.getName());
    }

    private boolean isIDTokenSigned() {
        return !JWSAlgorithm.NONE.getName().equals(signatureAlgorithm.getName());
    }

    private String getAuthorizationCode(OAuthTokenReqMessageContext tokenReqMsgCtxt) {
        return (String) tokenReqMsgCtxt.getProperty(AUTHORIZATION_CODE);
    }

    private boolean isLocalUser(AuthenticatedUser authorizedUser) {
        return !authorizedUser.isFederatedUser();
    }

    private String getSpTenantDomain(OAuthTokenReqMessageContext tokReqMsgCtx) {
        return tokReqMsgCtx.getOauth2AccessTokenReqDTO().getTenantDomain();
    }

    private void handleOIDCCustomClaims(OAuthTokenReqMessageContext tokReqMsgCtx, JWTClaimsSet jwtClaimsSet) {
        CustomClaimsCallbackHandler claimsCallBackHandler =
                OAuthServerConfiguration.getInstance().getOpenIDConnectCustomClaimsCallbackHandler();
        claimsCallBackHandler.handleCustomClaims(jwtClaimsSet, tokReqMsgCtx);
    }

    private String getSubjectClaimForLocalUser(ServiceProvider serviceProvider,
                                               AuthenticatedUser authorizedUser) throws IdentityOAuth2Exception {
        String subject;
        String username = authorizedUser.getUserName();
        String userStoreDomain = authorizedUser.getUserStoreDomain();
        String userTenantDomain = authorizedUser.getTenantDomain();

        String subjectClaimUri = getSubjectClaimUriInLocalDialect(serviceProvider);
        if (StringUtils.isNotBlank(subjectClaimUri)) {
            String fullQualifiedUsername = authorizedUser.toFullQualifiedUsername();
            try {
                subject = getSubjectClaimFromUserStore(subjectClaimUri, authorizedUser);
                if (StringUtils.isBlank(subject)) {
                    // Set username as the subject claim since we have no other option
                    subject = username;
                    log.warn("Cannot find subject claim: " + subjectClaimUri + " for user:" + fullQualifiedUsername
                            + ". Defaulting to username: " + subject + " as the subject identifier.");
                }
                // Get the subject claim in the correct format (ie. tenantDomain or userStoreDomain appended)
                subject = getFormattedSubjectClaim(serviceProvider, subject, userStoreDomain, userTenantDomain);
            } catch (IdentityException e) {
                String error = "Error occurred while getting user claim for user: " + authorizedUser + ", claim: " +
                        subjectClaimUri;
                throw new IdentityOAuth2Exception(error, e);
            } catch (UserStoreException e) {
                String error = "Error occurred while getting subject claim: " + subjectClaimUri + " for user: "
                        + fullQualifiedUsername;
                throw new IdentityOAuth2Exception(error, e);
            }
        } else {
            subject = getFormattedSubjectClaim(serviceProvider, username, userStoreDomain, userTenantDomain);
            if (log.isDebugEnabled()) {
                log.debug("No subject claim defined for service provider: " + serviceProvider.getApplicationName()
                        + ". Using username as the subject claim.");
            }
        }
        return subject;
    }

    private String getSubjectClaimFromUserStore(String subjectClaimUri, AuthenticatedUser authenticatedUser)
            throws UserStoreException, IdentityException {

        UserStoreManager userStoreManager = IdentityTenantUtil
                .getRealm(authenticatedUser.getTenantDomain(), authenticatedUser.toFullQualifiedUsername())
                .getUserStoreManager();

        return userStoreManager
                .getSecondaryUserStoreManager(authenticatedUser.getUserStoreDomain())
                .getUserClaimValue(authenticatedUser.getUserName(), subjectClaimUri, null);
    }

    private String getSubjectClaimUriInLocalDialect(ServiceProvider serviceProvider) {
        String subjectClaimUri = serviceProvider.getLocalAndOutBoundAuthenticationConfig().getSubjectClaimUri();
        if (log.isDebugEnabled()) {
            if (isNotBlank(subjectClaimUri)) {
                log.debug(subjectClaimUri + " is defined as subject claim for service provider: " +
                        serviceProvider.getApplicationName());
            } else {
                log.debug("No subject claim defined for service provider: " + serviceProvider.getApplicationName());
            }
        }
        // Get the local subject claim URI, if subject claim was a SP mapped one
        return getSubjectClaimUriInLocalDialect(serviceProvider, subjectClaimUri);
    }

    private String getFormattedSubjectClaim(ServiceProvider serviceProvider,
                                            String subjectClaimValue,
                                            String userStoreDomain,
                                            String tenantDomain) {

        boolean appendUserStoreDomainToSubjectClaim = serviceProvider.getLocalAndOutBoundAuthenticationConfig()
                .isUseUserstoreDomainInLocalSubjectIdentifier();

        boolean appendTenantDomainToSubjectClaim = serviceProvider.getLocalAndOutBoundAuthenticationConfig()
                .isUseTenantDomainInLocalSubjectIdentifier();

        if (appendTenantDomainToSubjectClaim) {
            subjectClaimValue = UserCoreUtil.addTenantDomainToEntry(subjectClaimValue, tenantDomain);
        }
        if (appendUserStoreDomainToSubjectClaim) {
            subjectClaimValue = IdentityUtil.addDomainToName(subjectClaimValue, userStoreDomain);
        }

        return subjectClaimValue;
    }

    private String getSigningTenantDomain(OAuthTokenReqMessageContext tokReqMsgCtx) {
        boolean isJWTSignedWithSPKey = OAuthServerConfiguration.getInstance().isJWTSignedWithSPKey();
        if (isJWTSignedWithSPKey) {
            return (String) tokReqMsgCtx.getProperty(MultitenantConstants.TENANT_DOMAIN);
        } else {
            return tokReqMsgCtx.getAuthorizedUser().getTenantDomain();
        }
    }

    private String getSubjectClaimUriInLocalDialect(ServiceProvider serviceProvider, String subjectClaimUri) {
        if (isNotBlank(subjectClaimUri)) {
            ClaimConfig claimConfig = serviceProvider.getClaimConfig();
            if (claimConfig != null) {
                boolean isLocalClaimDialect = claimConfig.isLocalClaimDialect();
                ClaimMapping[] claimMappings = claimConfig.getClaimMappings();
                if (!isLocalClaimDialect && ArrayUtils.isNotEmpty(claimMappings)) {
                    for (ClaimMapping claimMapping : claimMappings) {
                        if (StringUtils.equals(claimMapping.getRemoteClaim().getClaimUri(), subjectClaimUri)) {
                            return claimMapping.getLocalClaim().getClaimUri();
                        }
                    }
                }
            }
        }
        // This means the original subjectClaimUri passed was the subject claim URI.
        return subjectClaimUri;
    }

    private List<String> getOIDCAudience(String clientId, String tenantDomain) {
        List<String> oidcAudiences = getDefinedCustomOIDCAudiences(clientId, tenantDomain);
        // Need to add client_id as an audience value according to the spec.
        oidcAudiences.add(clientId);
        return oidcAudiences;
    }

    private ServiceProvider getServiceProvider(String spTenantDomain,
                                               String clientId) throws IdentityOAuth2Exception {
        ApplicationManagementService applicationMgtService = OAuth2ServiceComponentHolder.getApplicationMgtService();
        try {
            String spName =
                    applicationMgtService
                            .getServiceProviderNameByClientId(clientId, INBOUND_AUTH2_TYPE, spTenantDomain);
            return applicationMgtService.getApplicationExcludingFileBasedSPs(spName, spTenantDomain);
        } catch (IdentityApplicationManagementException e) {
            throw new IdentityOAuth2Exception("Error while getting service provider information for client_id: "
                    + clientId + " tenantDomain: " + spTenantDomain, e);
        }<commit_msg>Fix wso2/product-is#2952<commit_after>/*
 * Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 * 
 * WSO2 Inc. licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.wso2.carbon.identity.openidconnect;

import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
import org.apache.axiom.om.OMElement;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.base.MultitenantConstants;
import org.wso2.carbon.identity.application.authentication.framework.AuthenticationMethodNameTranslator;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser;
import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException;
import org.wso2.carbon.identity.application.common.model.ClaimConfig;
import org.wso2.carbon.identity.application.common.model.ClaimMapping;
import org.wso2.carbon.identity.application.common.model.FederatedAuthenticatorConfig;
import org.wso2.carbon.identity.application.common.model.IdentityProvider;
import org.wso2.carbon.identity.application.common.model.ServiceProvider;
import org.wso2.carbon.identity.application.common.util.IdentityApplicationConstants;
import org.wso2.carbon.identity.application.common.util.IdentityApplicationManagementUtil;
import org.wso2.carbon.identity.application.mgt.ApplicationManagementService;
import org.wso2.carbon.identity.base.IdentityConstants;
import org.wso2.carbon.identity.base.IdentityException;
import org.wso2.carbon.identity.core.util.IdentityConfigParser;
import org.wso2.carbon.identity.core.util.IdentityCoreConstants;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCache;
import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCacheEntry;
import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCacheKey;
import org.wso2.carbon.identity.oauth.cache.CacheEntry;
import org.wso2.carbon.identity.oauth.cache.OIDCAudienceCache;
import org.wso2.carbon.identity.oauth.cache.OIDCAudienceCacheEntry;
import org.wso2.carbon.identity.oauth.cache.OIDCAudienceCacheKey;
import org.wso2.carbon.identity.oauth.common.OAuthConstants;
import org.wso2.carbon.identity.oauth.common.exception.InvalidOAuthClientException;
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
import org.wso2.carbon.identity.oauth.dao.OAuthAppDAO;
import org.wso2.carbon.identity.oauth.dao.OAuthAppDO;
import org.wso2.carbon.identity.oauth2.IDTokenValidationFailureException;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext;
import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenRespDTO;
import org.wso2.carbon.identity.oauth2.dto.OAuth2AuthorizeReqDTO;
import org.wso2.carbon.identity.oauth2.dto.OAuth2AuthorizeRespDTO;
import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder;
import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext;
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;
import org.wso2.carbon.identity.openidconnect.internal.OpenIDConnectServiceComponentHolder;
import org.wso2.carbon.idp.mgt.IdentityProviderManagementException;
import org.wso2.carbon.idp.mgt.IdentityProviderManager;
import org.wso2.carbon.user.core.UserStoreException;
import org.wso2.carbon.user.core.UserStoreManager;
import org.wso2.carbon.user.core.util.UserCoreUtil;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;

import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.OIDCClaims.AUTH_TIME;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.OIDCClaims.AZP;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.OIDCClaims.NONCE;

/**
 * Default IDToken generator for the OpenID Connect Implementation.
 * This IDToken Generator utilizes the Nimbus SDK to build the IDToken.
 */
public class DefaultIDTokenBuilder implements org.wso2.carbon.identity.openidconnect.IDTokenBuilder {

    private static final String SHA384 = "SHA-384";
    private static final String SHA512 = "SHA-512";
    private static final String AUTHORIZATION_CODE = "AuthorizationCode";
    private static final String INBOUND_AUTH2_TYPE = "oauth2";
    private static final String CONFIG_ELEM_OAUTH = "OAuth";
    private static final String OPENID_CONNECT = "OpenIDConnect";
    private static final String OPENID_CONNECT_AUDIENCES = "Audiences";
    private static final String OPENID_CONNECT_AUDIENCE = "Audience";
    private static final String OPENID_IDP_ENTITY_ID = "IdPEntityId";

    private static final Log log = LogFactory.getLog(DefaultIDTokenBuilder.class);
    private JWSAlgorithm signatureAlgorithm;
    private JWEAlgorithm encryptionAlgorithm;
    private EncryptionMethod encryptionMethod;

    public DefaultIDTokenBuilder() throws IdentityOAuth2Exception {
        // Map signature algorithm from identity.xml to nimbus format, this is a one time configuration.
        signatureAlgorithm = OAuth2Util.mapSignatureAlgorithmForJWSAlgorithm(
                OAuthServerConfiguration.getInstance().getIdTokenSignatureAlgorithm());
    }

    /**
     * Create an oAuthAppDO object using client id and set encryption algorithm and encryption method.
     * @param clientId  ID of the client.
     * @throws IdentityOAuth2Exception
     */
    private void setupEncryptionAlgorithms(OAuthAppDO oAuthAppDO, String clientId) throws IdentityOAuth2Exception {

        encryptionAlgorithm = OAuth2Util.mapEncryptionAlgorithmForJWEAlgorithm(
                    oAuthAppDO.getIdTokenEncryptionAlgorithm());
        encryptionMethod = OAuth2Util.mapEncryptionMethodForJWEAlgorithm(oAuthAppDO.getIdTokenEncryptionMethod());

        if (log.isDebugEnabled()) {
            log.debug("Id token encryption is enabled using encryption algorithm: " + encryptionAlgorithm +
                    " and encryption method: " + encryptionMethod + ", for client: " + clientId);
        }
    }

    @Override
    public String buildIDToken(OAuthTokenReqMessageContext tokenReqMsgCtxt,
                               OAuth2AccessTokenRespDTO tokenRespDTO) throws IdentityOAuth2Exception {
        String clientId = tokenReqMsgCtxt.getOauth2AccessTokenReqDTO().getClientId();
        String spTenantDomain = getSpTenantDomain(tokenReqMsgCtxt);
        String idTokenIssuer = getIdTokenIssuer(spTenantDomain);
        String accessToken = tokenRespDTO.getAccessToken();

        long idTokenValidityInMillis = getIDTokenExpiryInMillis();
        long currentTimeInMillis = Calendar.getInstance().getTimeInMillis();

        AuthenticatedUser authorizedUser = tokenReqMsgCtxt.getAuthorizedUser();
        String subjectClaim = getSubjectClaim(tokenReqMsgCtxt, tokenRespDTO, clientId, spTenantDomain, authorizedUser);

        String nonceValue = null;
        long authTime = 0;
        String acrValue = null;
        List<String> amrValues = Collections.emptyList();

        // AuthorizationCode only available for authorization code grant type
        if (getAuthorizationCode(tokenReqMsgCtxt) != null) {
            AuthorizationGrantCacheEntry authzGrantCacheEntry = getAuthorizationGrantCacheEntryFromToken
                    (tokenRespDTO.getAccessToken());
            if (authzGrantCacheEntry != null) {
                nonceValue = authzGrantCacheEntry.getNonceValue();
                acrValue = authzGrantCacheEntry.getSelectedAcrValue();
                if (isAuthTimeRequired(authzGrantCacheEntry)) {
                    authTime = authzGrantCacheEntry.getAuthTime();
                }
                amrValues = authzGrantCacheEntry.getAmrList();
            }
        } else {
            amrValues = tokenReqMsgCtxt.getOauth2AccessTokenReqDTO().getAuthenticationMethodReferences();
        }

        if (log.isDebugEnabled()) {
            log.debug(buildDebugMessage(idTokenIssuer, subjectClaim, nonceValue, idTokenValidityInMillis,
                    currentTimeInMillis));
        }

        List<String> audience = getOIDCAudience(clientId, spTenantDomain);

        JWTClaimsSet jwtClaimsSet = new JWTClaimsSet();
        jwtClaimsSet.setIssuer(idTokenIssuer);
        jwtClaimsSet.setAudience(audience);
        jwtClaimsSet.setClaim(AZP, clientId);
        jwtClaimsSet.setExpirationTime(getIdTokenExpiryInMillis(idTokenValidityInMillis, currentTimeInMillis));
        jwtClaimsSet.setIssueTime(new Date(currentTimeInMillis));
        if (authTime != 0) {
            jwtClaimsSet.setClaim(AUTH_TIME, authTime / 1000);
        }
        if (nonceValue != null) {
            jwtClaimsSet.setClaim(NONCE, nonceValue);
        }
        if (StringUtils.isNotEmpty(acrValue)) {
            jwtClaimsSet.setClaim(OAuthConstants.ACR, acrValue);
        }
        if (amrValues != null) {
            jwtClaimsSet.setClaim(OAuthConstants.AMR, translateAmrToResponse(amrValues));
        }

        setAdditionalClaims(tokenReqMsgCtxt, tokenRespDTO, jwtClaimsSet);

        tokenReqMsgCtxt.addProperty(OAuthConstants.ACCESS_TOKEN, accessToken);
        tokenReqMsgCtxt.addProperty(MultitenantConstants.TENANT_DOMAIN, getSpTenantDomain(tokenReqMsgCtxt));

        handleOIDCCustomClaims(tokenReqMsgCtxt, jwtClaimsSet);
        jwtClaimsSet.setSubject(subjectClaim);

        if (isInvalidToken(jwtClaimsSet)) {
            throw new IDTokenValidationFailureException("Error while validating ID Token token for required claims");
        }

        if (isUnsignedIDToken()) {
            return new PlainJWT(jwtClaimsSet).serialize();
        }

        // Initialize OAuthAppDO using the client ID.
        OAuthAppDO oAuthAppDO;
        try {
            oAuthAppDO = OAuth2Util.getAppInformationByClientId(clientId);
        } catch (InvalidOAuthClientException e) {
            String error = "Error occurred while getting app information for client_id: " + clientId;
            throw new IdentityOAuth2Exception(error, e);
        }

        return getIDToken(clientId, spTenantDomain, jwtClaimsSet, oAuthAppDO, getSigningTenantDomain(tokenReqMsgCtxt));
    }

    @Override
    public String buildIDToken(OAuthAuthzReqMessageContext authzReqMessageContext,
                               OAuth2AuthorizeRespDTO tokenRespDTO) throws IdentityOAuth2Exception {

        String accessToken = tokenRespDTO.getAccessToken();
        String clientId = authzReqMessageContext.getAuthorizationReqDTO().getConsumerKey();
        String spTenantDomain = getSpTenantDomain(authzReqMessageContext);
        String issuer = getIdTokenIssuer(spTenantDomain);

        // Get subject from Authenticated Subject Identifier
        AuthenticatedUser authorizedUser = authzReqMessageContext.getAuthorizationReqDTO().getUser();
        String subject =
                getSubjectClaim(authzReqMessageContext, tokenRespDTO, clientId, spTenantDomain, authorizedUser);

        String nonceValue = authzReqMessageContext.getAuthorizationReqDTO().getNonce();
        String acrValue = authzReqMessageContext.getAuthorizationReqDTO().getSelectedAcr();
        List<String> amrValues = Collections.emptyList(); //TODO:

        long idTokenLifeTimeInMillis = getIDTokenExpiryInMillis();
        long currentTimeInMillis = Calendar.getInstance().getTimeInMillis();

        if (log.isDebugEnabled()) {
            log.debug(buildDebugMessage(issuer, subject, nonceValue, idTokenLifeTimeInMillis, currentTimeInMillis));
        }

        JWTClaimsSet jwtClaimsSet = new JWTClaimsSet();
        jwtClaimsSet.setIssuer(issuer);

        // Set the audience
        List<String> audience = getOIDCAudience(clientId, spTenantDomain);
        jwtClaimsSet.setAudience(audience);

        jwtClaimsSet.setClaim(AZP, clientId);
        jwtClaimsSet.setExpirationTime(getIdTokenExpiryInMillis(idTokenLifeTimeInMillis, currentTimeInMillis));
        jwtClaimsSet.setIssueTime(new Date(currentTimeInMillis));

        long authTime = getAuthTime(authzReqMessageContext);
        if (authTime != 0) {
            jwtClaimsSet.setClaim(AUTH_TIME, authTime / 1000);
        }
        if (nonceValue != null) {
            jwtClaimsSet.setClaim(OAuthConstants.OIDCClaims.NONCE, nonceValue);
        }
        if (StringUtils.isNotEmpty(acrValue)) {
            jwtClaimsSet.setClaim("acr", acrValue);
        }
        if (amrValues != null) {
            jwtClaimsSet.setClaim("amr", translateAmrToResponse(amrValues));
        }

        setAdditionalClaims(authzReqMessageContext, tokenRespDTO, jwtClaimsSet);

        authzReqMessageContext.addProperty(OAuthConstants.ACCESS_TOKEN, accessToken);
        authzReqMessageContext
                .addProperty(MultitenantConstants.TENANT_DOMAIN, getSpTenantDomain(authzReqMessageContext));
        handleCustomOIDCClaims(authzReqMessageContext, jwtClaimsSet);
        jwtClaimsSet.setSubject(subject);

        if (isUnsignedIDToken()) {
            return new PlainJWT(jwtClaimsSet).serialize();
        }

        // Initialize OAuthAppDO using the client ID.
        OAuthAppDO oAuthAppDO;
        try {
            oAuthAppDO = OAuth2Util.getAppInformationByClientId(clientId);
        } catch (InvalidOAuthClientException e) {
            String error = "Error occurred while getting app information for client_id: " + clientId;
            throw new IdentityOAuth2Exception(error, e);
        }

        return getIDToken(clientId, spTenantDomain, jwtClaimsSet, oAuthAppDO,
                getSigningTenantDomain(authzReqMessageContext));
    }

    private String getIDToken(String clientId, String spTenantDomain, JWTClaimsSet jwtClaimsSet, OAuthAppDO oAuthAppDO,
                              String signingTenantDomain) throws IdentityOAuth2Exception {

        if (oAuthAppDO.isIdTokenEncryptionEnabled()) {
            setupEncryptionAlgorithms(oAuthAppDO, clientId);
            return OAuth2Util.encryptJWT(jwtClaimsSet, encryptionAlgorithm, encryptionMethod, spTenantDomain,
                    clientId).serialize();
        } else {
            return OAuth2Util.signJWT(jwtClaimsSet, signatureAlgorithm, signingTenantDomain).serialize();
        }
    }

    protected String getSubjectClaim(OAuthTokenReqMessageContext tokenReqMessageContext,
                                     OAuth2AccessTokenRespDTO tokenRespDTO,
                                     String clientId,
                                     String spTenantDomain,
                                     AuthenticatedUser authorizedUser) throws IdentityOAuth2Exception {
        String accessToken = tokenRespDTO.getAccessToken();
        String subjectClaim = OIDCClaimUtil.getSubjectClaimCachedAgainstAccessToken(accessToken);
        if (isNotBlank(subjectClaim)) {
            if (log.isDebugEnabled()) {
                if (IdentityUtil.isTokenLoggable(IdentityConstants.IdentityTokens.USER_CLAIMS)) {
                    log.debug("Subject claim cached against the access token found for user: " + authorizedUser);
                } else {
                    log.debug("Subject claim: " + subjectClaim + " cached against the access token found for user: " +
                            authorizedUser);
                }
            }
            return subjectClaim;
        }
        return getSubjectClaim(clientId, spTenantDomain, authorizedUser);
    }

    protected String getSubjectClaim(OAuthAuthzReqMessageContext authzReqMessageContext,
                                     OAuth2AuthorizeRespDTO authorizeRespDTO,
                                     String clientId,
                                     String spTenantDomain,
                                     AuthenticatedUser authorizedUser) throws IdentityOAuth2Exception {
        String accessToken = authorizeRespDTO.getAccessToken();
        String subjectClaim = OIDCClaimUtil.getSubjectClaimCachedAgainstAccessToken(accessToken);
        if (isNotBlank(subjectClaim)) {
            if (log.isDebugEnabled()) {
                if (IdentityUtil.isTokenLoggable(IdentityConstants.IdentityTokens.USER_CLAIMS)) {
                    log.debug("Subject claim cached against the authz code found for user: " + authorizedUser);
                } else {
                    log.debug("Subject claim: " + subjectClaim + " cached against the authz code found for user: " +
                            authorizedUser);
                }
            }
            return subjectClaim;
        }
        return getSubjectClaim(clientId, spTenantDomain, authorizedUser);
    }

    private String getSubjectClaim(String clientId,
                                   String spTenantDomain,
                                   AuthenticatedUser authorizedUser) throws IdentityOAuth2Exception {
        String subjectClaim;
        if (isLocalUser(authorizedUser)) {
            // If the user is local then we need to find the subject claim of the user defined in SP configs and
            // append userStoreDomain/tenantDomain as configured
            ServiceProvider serviceProvider = getServiceProvider(spTenantDomain, clientId);
            if (serviceProvider == null) {
                throw new IdentityOAuth2Exception("Cannot find an service provider for client_id: " + clientId + " " +
                        "in tenantDomain: " + spTenantDomain);
            }
            subjectClaim = getSubjectClaimForLocalUser(serviceProvider, authorizedUser);
            if (log.isDebugEnabled()) {
                log.debug("Subject claim: " + subjectClaim + " set for local user: " + authorizedUser + " for " +
                        "application: " + clientId + " of tenantDomain: " + spTenantDomain);
            }
        } else {
            subjectClaim = authorizedUser.getAuthenticatedSubjectIdentifier();
            if (log.isDebugEnabled()) {
                log.debug("Subject claim: " + subjectClaim + " set for federated user: " + authorizedUser + " for " +
                        "application: " + clientId + " of tenantDomain: " + spTenantDomain);
            }
        }
        return subjectClaim;
    }

    private String buildDebugMessage(String issuer, String subject, String nonceValue, long idTokenLifeTimeInMillis,
                                     long currentTimeInMillis) {
        return (new StringBuilder())
                .append("Using issuer ").append(issuer).append("\n")
                .append("Subject ").append(subject).append("\n")
                .append("ID Token life time ").append(idTokenLifeTimeInMillis / 1000).append("\n")
                .append("Current time ").append(currentTimeInMillis / 1000).append("\n")
                .append("Nonce Value ").append(nonceValue).append("\n")
                .append("Signature Algorithm ").append(signatureAlgorithm).append("\n")
                .toString();
    }

    private boolean isInvalidToken(JWTClaimsSet jwtClaimsSet) {
        return !isValidIdToken(jwtClaimsSet);
    }

    private boolean isEssentialClaim(AuthorizationGrantCacheEntry authorizationGrantCacheEntry, String oidcClaimUri) {
        return isEssentialClaim(authorizationGrantCacheEntry.getEssentialClaims(), oidcClaimUri);
    }

    private boolean isEssentialClaim(String essentialClaims, String oidcClaimUri) {
        return StringUtils.isNotBlank(essentialClaims) &&
                OAuth2Util.getEssentialClaims(essentialClaims, OAuthConstants.ID_TOKEN).contains(oidcClaimUri);
    }

    private boolean isMaxAgePresentInAuthzRequest(AuthorizationGrantCacheEntry authorizationGrantCacheEntry) {
        return authorizationGrantCacheEntry.getMaxAge() != 0;
    }

    private boolean isUnsignedIDToken() {
        return JWSAlgorithm.NONE.getName().equals(signatureAlgorithm.getName());
    }

    private boolean isIDTokenSigned() {
        return !JWSAlgorithm.NONE.getName().equals(signatureAlgorithm.getName());
    }

    private String getAuthorizationCode(OAuthTokenReqMessageContext tokenReqMsgCtxt) {
        return (String) tokenReqMsgCtxt.getProperty(AUTHORIZATION_CODE);
    }

    private boolean isLocalUser(AuthenticatedUser authorizedUser) {
        return !authorizedUser.isFederatedUser();
    }

    private String getSpTenantDomain(OAuthTokenReqMessageContext tokReqMsgCtx) {
        return tokReqMsgCtx.getOauth2AccessTokenReqDTO().getTenantDomain();
    }

    private void handleOIDCCustomClaims(OAuthTokenReqMessageContext tokReqMsgCtx, JWTClaimsSet jwtClaimsSet) {
        CustomClaimsCallbackHandler claimsCallBackHandler =
                OAuthServerConfiguration.getInstance().getOpenIDConnectCustomClaimsCallbackHandler();
        claimsCallBackHandler.handleCustomClaims(jwtClaimsSet, tokReqMsgCtx);
    }

    private String getSubjectClaimForLocalUser(ServiceProvider serviceProvider,
                                               AuthenticatedUser authorizedUser) throws IdentityOAuth2Exception {
        String subject;
        String username = authorizedUser.getUserName();
        String userStoreDomain = authorizedUser.getUserStoreDomain();
        String userTenantDomain = authorizedUser.getTenantDomain();

        String subjectClaimUri = getSubjectClaimUriInLocalDialect(serviceProvider);
        if (StringUtils.isNotBlank(subjectClaimUri)) {
            String fullQualifiedUsername = authorizedUser.toFullQualifiedUsername();
            try {
                subject = getSubjectClaimFromUserStore(subjectClaimUri, authorizedUser);
                if (StringUtils.isBlank(subject)) {
                    // Set username as the subject claim since we have no other option
                    subject = username;
                    log.warn("Cannot find subject claim: " + subjectClaimUri + " for user:" + fullQualifiedUsername
                            + ". Defaulting to username: " + subject + " as the subject identifier.");
                }
                // Get the subject claim in the correct format (ie. tenantDomain or userStoreDomain appended)
                subject = getFormattedSubjectClaim(serviceProvider, subject, userStoreDomain, userTenantDomain);
            } catch (IdentityException e) {
                String error = "Error occurred while getting user claim for user: " + authorizedUser + ", claim: " +
                        subjectClaimUri;
                throw new IdentityOAuth2Exception(error, e);
            } catch (UserStoreException e) {
                String error = "Error occurred while getting subject claim: " + subjectClaimUri + " for user: "
                        + fullQualifiedUsername;
                throw new IdentityOAuth2Exception(error, e);
            }
        } else {
            subject = getFormattedSubjectClaim(serviceProvider, username, userStoreDomain, userTenantDomain);
            if (log.isDebugEnabled()) {
                log.debug("No subject claim defined for service provider: " + serviceProvider.getApplicationName()
                        + ". Using username as the subject claim.");
            }
        }
        return subject;
    }

    private String getSubjectClaimFromUserStore(String subjectClaimUri, AuthenticatedUser authenticatedUser)
            throws UserStoreException, IdentityException {

        UserStoreManager userStoreManager = IdentityTenantUtil
                .getRealm(authenticatedUser.getTenantDomain(), authenticatedUser.toFullQualifiedUsername())
                .getUserStoreManager();

        return userStoreManager
                .getSecondaryUserStoreManager(authenticatedUser.getUserStoreDomain())
                .getUserClaimValue(authenticatedUser.getUserName(), subjectClaimUri, null);
    }

    private String getSubjectClaimUriInLocalDialect(ServiceProvider serviceProvider) {
        String subjectClaimUri = serviceProvider.getLocalAndOutBoundAuthenticationConfig().getSubjectClaimUri();
        if (log.isDebugEnabled()) {
            if (isNotBlank(subjectClaimUri)) {
                log.debug(subjectClaimUri + " is defined as subject claim for service provider: " +
                        serviceProvider.getApplicationName());
            } else {
                log.debug("No subject claim defined for service provider: " + serviceProvider.getApplicationName());
            }
        }
        // Get the local subject claim URI, if subject claim was a SP mapped one
        return getSubjectClaimUriInLocalDialect(serviceProvider, subjectClaimUri);
    }

    private String getFormattedSubjectClaim(ServiceProvider serviceProvider,
                                            String subjectClaimValue,
                                            String userStoreDomain,
                                            String tenantDomain) {

        boolean appendUserStoreDomainToSubjectClaim = serviceProvider.getLocalAndOutBoundAuthenticationConfig()
                .isUseUserstoreDomainInLocalSubjectIdentifier();

        boolean appendTenantDomainToSubjectClaim = serviceProvider.getLocalAndOutBoundAuthenticationConfig()
                .isUseTenantDomainInLocalSubjectIdentifier();

        if (appendTenantDomainToSubjectClaim) {
            subjectClaimValue = UserCoreUtil.addTenantDomainToEntry(subjectClaimValue, tenantDomain);
        }
        if (appendUserStoreDomainToSubjectClaim) {
            subjectClaimValue = IdentityUtil.addDomainToName(subjectClaimValue, userStoreDomain);
        }

        return subjectClaimValue;
    }

    private String getSigningTenantDomain(OAuthTokenReqMessageContext tokReqMsgCtx) {
        boolean isJWTSignedWithSPKey = OAuthServerConfiguration.getInstance().isJWTSignedWithSPKey();
        if (isJWTSignedWithSPKey) {
            return (String) tokReqMsgCtx.getProperty(MultitenantConstants.TENANT_DOMAIN);
        } else {
            return tokReqMsgCtx.getAuthorizedUser().getTenantDomain();
        }
    }

    private String getSubjectClaimUriInLocalDialect(ServiceProvider serviceProvider, String subjectClaimUri) {
        if (isNotBlank(subjectClaimUri)) {
            ClaimConfig claimConfig = serviceProvider.getClaimConfig();
            if (claimConfig != null) {
                boolean isLocalClaimDialect = claimConfig.isLocalClaimDialect();
                ClaimMapping[] claimMappings = claimConfig.getClaimMappings();
                if (!isLocalClaimDialect && ArrayUtils.isNotEmpty(claimMappings)) {
                    for (ClaimMapping claimMapping : claimMappings) {
                        if (StringUtils.equals(claimMapping.getRemoteClaim().getClaimUri(), subjectClaimUri)) {
                            return claimMapping.getLocalClaim().getClaimUri();
                        }
                    }
                }
            }
        }
        // This means the original subjectClaimUri passed was the subject claim URI.
        return subjectClaimUri;
    }

    private List<String> getOIDCAudience(String clientId, String tenantDomain) {
        List<String> oidcAudiences = getDefinedCustomOIDCAudiences(clientId, tenantDomain);
        // Need to add client_id as an audience value according to the spec.
        if (!oidcAudiences.contains(clientId)) {
            oidcAudiences.add(clientId);
        }
        return oidcAudiences;
    }

    private ServiceProvider getServiceProvider(String spTenantDomain,
                                               String clientId) throws IdentityOAuth2Exception {
        ApplicationManagementService applicationMgtService = OAuth2ServiceComponentHolder.getApplicationMgtService();
        try {
            String spName =
                    applicationMgtService
                            .getServiceProviderNameByClientId(clientId, INBOUND_AUTH2_TYPE, spTenantDomain);
            return applicationMgtService.getApplicationExcludingFileBasedSPs(spName, spTenantDomain);
        } catch (IdentityApplicationManagementException e) {
            throw new IdentityOAuth2Exception("Error while getting service provider information for client_id: "
                    + clientId + " tenantDomain: " + spTenantDomain, e);
        }
{'fraction_non_alphanumeric': 0.07650714089535841, 'fraction_numerical': 0.0050295248558088436, 'mean_word_length': 3.009704728474086, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 44, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '540457', 'n_tokens_mistral': 15588, 'n_tokens_neox': 15081, 'n_words': 2931}