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

pl.edu.icm.unity.oauth.as.OAuthProcessor Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2014 ICM Uniwersytet Warszawski All rights reserved.
 * See LICENCE.txt file for licensing information.
 */
package pl.edu.icm.unity.oauth.as;

import static com.nimbusds.openid.connect.sdk.OIDCResponseTypeValue.ID_TOKEN;

import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jwt.JWT;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.AuthorizationSuccessResponse;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.id.Audience;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.oauth2.sdk.id.Subject;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
import com.nimbusds.openid.connect.sdk.OIDCResponseTypeValue;
import com.nimbusds.openid.connect.sdk.claims.AccessTokenHash;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
import com.nimbusds.openid.connect.sdk.claims.CodeHash;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;

import pl.edu.icm.unity.base.attribute.Attribute;
import pl.edu.icm.unity.base.endpoint.idp.IdpStatistic.Status;
import pl.edu.icm.unity.base.entity.EntityParam;
import pl.edu.icm.unity.base.exceptions.EngineException;
import pl.edu.icm.unity.base.identity.IdentityParam;
import pl.edu.icm.unity.base.message.MessageSource;
import pl.edu.icm.unity.engine.api.attributes.DynamicAttribute;
import pl.edu.icm.unity.engine.api.token.TokensManagement;
import pl.edu.icm.unity.engine.api.translation.out.TranslationResult;
import pl.edu.icm.unity.oauth.as.OAuthSystemAttributesProvider.GrantFlow;
import pl.edu.icm.unity.oauth.as.OAuthToken.PKCSInfo;
import pl.edu.icm.unity.oauth.as.token.access.AccessTokenFactory;
import pl.edu.icm.unity.oauth.as.token.access.OAuthAccessTokenRepository;

/**
 * Groups OAuth related logic for processing the request and preparing the response.  
 * @author K. Benedyczak
 */
@Component
public class OAuthProcessor
{
	public static final String INTERNAL_CODE_TOKEN = "oauth2Code";

	
	
	private final TokensManagement tokensMan;
	private final OAuthAccessTokenRepository tokenDAO;

	@Autowired
	public OAuthProcessor(TokensManagement tokensMan, OAuthAccessTokenRepository tokenDAO,
			ApplicationEventPublisher eventPublisher, MessageSource msg)
	{
		this.tokensMan = tokensMan;
		this.tokenDAO = tokenDAO;
	}

	/**
	 * Returns only requested attributes for which we have mapping.
	 */
	public static Set filterAttributes(TranslationResult userInfo, 
			Set requestedAttributes)
	{
		Set ret = filterNotRequestedAttributes(userInfo, requestedAttributes);
		return filterUnsupportedAttributes(ret);
	}

	/**
	 * Returns Authorization response to be returned and records (if needed) 
	 * the internal state token, which is needed to associate further use of the code and/or id tokens with
	 * the authorization that currently takes place.
	 */
	public AuthorizationSuccessResponse prepareAuthzResponseAndRecordInternalState(
			Collection attributes,
			IdentityParam identity,	OAuthAuthzContext ctx, OAuthIdpStatisticReporter statReporter) 
					throws EngineException, JsonProcessingException, ParseException, JOSEException
	{
		OAuthToken internalToken = new OAuthToken();
		OAuthASProperties config = ctx.getConfig();
		internalToken.setEffectiveScope(ctx.getEffectiveRequestedScopesList());
		internalToken.setRequestedScope(ctx.getRequestedScopes().stream().toArray(String[]::new));
		internalToken.setClientId(ctx.getClientEntityId());
		internalToken.setRedirectUri(ctx.getReturnURI().toASCIIString());
		internalToken.setClientName(ctx.getClientName());
		internalToken.setClientUsername(ctx.getClientUsername());
		internalToken.setSubject(identity.getValue());
		internalToken.setMaxExtendedValidity(config.getMaxExtendedAccessTokenValidity());
		internalToken.setTokenValidity(config.getAccessTokenValidity()); 
		internalToken.setAudience(Stream.concat(Stream.of(ctx.getClientUsername()), ctx.getAdditionalAudience().stream()).collect(Collectors.toList()));
		internalToken.setIssuerUri(config.getIssuerName());
		internalToken.setClientType(ctx.getClientType());
		internalToken.setClaimsInTokenAttribute(ctx.getClaimsInTokenAttribute());
		
		String codeChallenge = ctx.getRequest().getCodeChallenge() == null ? 
				null : ctx.getRequest().getCodeChallenge().getValue();
		String codeChallengeMethod = ctx.getRequest().getCodeChallengeMethod() == null ? 
				null : ctx.getRequest().getCodeChallengeMethod().getValue();
		PKCSInfo pkcsInfo = new PKCSInfo(codeChallenge, codeChallengeMethod);
		internalToken.setPkcsInfo(pkcsInfo);
	
		Date now = new Date();
		
		ResponseType responseType = ctx.getRequest().getResponseType();
		internalToken.setResponseType(responseType.toString());
		
		UserInfo userInfo = prepareUserInfoClaimSet(identity.getValue(), attributes);
		internalToken.setUserInfo(userInfo.toJSONObject().toJSONString());
		
		Optional idToken = generateIdTokenIfRequested(config, ctx, responseType, 
				internalToken, identity, userInfo, now);
		TokenSigner tokenSigner = config.getTokenSigner();
		JWSAlgorithm signingAlgorithm = tokenSigner.isPKIEnabled() ? 
				tokenSigner.getSigningAlgorithm() : null;
		Curve curve = tokenSigner.getCurve();
		
		AuthorizationSuccessResponse oauthResponse = null;
		AccessTokenFactory accessTokenFactory = new AccessTokenFactory(config);
		if (GrantFlow.authorizationCode == ctx.getFlow())
		{
			AuthorizationCode authzCode = new AuthorizationCode();
			internalToken.setAuthzCode(authzCode.getValue());
			
			signAndRecordIdToken(idToken, tokenSigner, responseType, internalToken);
			
			oauthResponse = new AuthorizationSuccessResponse(ctx.getReturnURI(), authzCode, null,
					ctx.getRequest().getState(), ctx.getRequest().impliedResponseMode());
			Date expiration = new Date(now.getTime() + config.getCodeTokenValidity() * 1000);
			tokensMan.addToken(INTERNAL_CODE_TOKEN, authzCode.getValue(), 
					new EntityParam(identity), internalToken.getSerialized(), now, expiration);
		} else if (GrantFlow.implicit == ctx.getFlow())
		{
			if (responseType.contains(OIDCResponseTypeValue.ID_TOKEN) && responseType.size() == 1)
			{
				Optional idTokenSigned = signAndRecordIdToken(idToken, tokenSigner, 
						responseType, internalToken);
				//we return only the id token, no access token so we don't need an internal token.
				return new AuthenticationSuccessResponse(
						ctx.getReturnURI(), null, idTokenSigned.orElse(null), 
						null, ctx.getRequest().getState(), null, 
						ctx.getRequest().impliedResponseMode());
			}

			AccessToken accessToken = accessTokenFactory.create(internalToken, now);
			internalToken.setAccessToken(accessToken.getValue());
			
			addAccessTokenHashIfNeededToIdToken(idToken, accessToken, signingAlgorithm, responseType, curve);
			Optional idTokenSigned = signAndRecordIdToken(idToken, tokenSigner, 
					responseType, internalToken);
			
			Date expiration = new Date(now.getTime() + config.getAccessTokenValidity() * 1000);
			oauthResponse = new AuthenticationSuccessResponse(
						ctx.getReturnURI(), null, idTokenSigned.orElse(null), 
						accessToken, ctx.getRequest().getState(), null, 
						ctx.getRequest().impliedResponseMode());
			statReporter.reportStatus(ctx, Status.SUCCESSFUL);
			tokenDAO.storeAccessToken(accessToken, internalToken, new EntityParam(identity), now, expiration);
		} else if (GrantFlow.openidHybrid == ctx.getFlow())
		{
			//in hybrid mode authz code is returned always
			AuthorizationCode authzCode = new AuthorizationCode();
			internalToken.setAuthzCode(authzCode.getValue());
			Date codeExpiration = new Date(now.getTime() + config.getCodeTokenValidity() * 1000);
			addCodeHashIfNeededToIdToken(idToken, authzCode, signingAlgorithm, responseType, curve);

			signAndRecordIdToken(idToken, tokenSigner, responseType, internalToken);
			tokensMan.addToken(INTERNAL_CODE_TOKEN, authzCode.getValue(), 
					new EntityParam(identity), internalToken.getSerialized(), 
					now, codeExpiration);
			
			//access token - sometimes
			AccessToken accessToken = null;
			if (responseType.contains(ResponseType.Value.TOKEN))
			{
				accessToken = accessTokenFactory.create(internalToken, now);
				internalToken.setAccessToken(accessToken.getValue());
				Date accessExpiration = new Date(now.getTime() + config.getAccessTokenValidity() * 1000);
				addAccessTokenHashIfNeededToIdToken(idToken, accessToken, signingAlgorithm, responseType, curve);
				
				signAndRecordIdToken(idToken, tokenSigner, responseType, internalToken);
				statReporter.reportStatus(ctx, Status.SUCCESSFUL);
				tokenDAO.storeAccessToken(accessToken, internalToken, new EntityParam(identity), now, 
						accessExpiration);
			}
			
			Optional idTokenSigned = signAndRecordIdToken(idToken, tokenSigner, 
					responseType, internalToken);

			oauthResponse = new AuthenticationSuccessResponse(
					ctx.getReturnURI(), authzCode, idTokenSigned.orElse(null), 
					accessToken, ctx.getRequest().getState(), null, 
					ctx.getRequest().impliedResponseMode());
		}
		
		return oauthResponse;
	}
	
	private Optional generateIdTokenIfRequested(OAuthASProperties config, OAuthAuthzContext ctx, 
			ResponseType responseType, OAuthToken internalToken, IdentityParam identity, 
			UserInfo userInfo, Date now) throws ParseException, JOSEException
	{
		return Optional.ofNullable(ctx.isOpenIdMode() ? 
				prepareIdInfoClaimSet(identity.getValue(), internalToken.getAudience(), ctx, userInfo, now) 
				: null);
	}

	private Optional signAndRecordIdToken(Optional idToken, TokenSigner tokenSigner, 
			ResponseType responseType, OAuthToken internalToken) throws ParseException, JOSEException
	{
		if (!idToken.isPresent())
			return Optional.empty();
		JWT idTokenSigned = tokenSigner.sign(idToken.get());	
		internalToken.setOpenidToken(idTokenSigned.serialize());
		//we record OpenID token in internal state always in open id mode. However it may happen
		//that it is not requested immediately now
		if (!responseType.contains(OIDCResponseTypeValue.ID_TOKEN))
			idTokenSigned = null;
		return Optional.ofNullable(idTokenSigned);
	}
	
	/**
	 * Returns a collection of attributes including only those attributes for which there is an OAuth 
	 * representation.
	 */
	private static Set filterUnsupportedAttributes(Set src)
	{
		Set ret = new HashSet<>();
		OAuthAttributeMapper mapper = new DefaultOAuthAttributeMapper();
		
		for (DynamicAttribute a: src)
			if (mapper.isHandled(a.getAttribute()))
				ret.add(a);
		return ret;
	}
	
	
	private static Set filterNotRequestedAttributes(TranslationResult translationResult, 
			Set requestedAttributes)
	{
		Collection allAttrs = translationResult.getAttributes();
		Set filteredAttrs = new HashSet<>();
		
		for (DynamicAttribute attr: allAttrs)
			if (requestedAttributes.contains(attr.getAttribute().getName()))
				filteredAttrs.add(attr);
		return filteredAttrs;
	}
	
	
	/**
	 * Creates an OIDC ID Token. The token includes regular attributes if and only if the access token is 
	 * not issued in the flow. This is the case if the only response type is 'id_token'. Section 5.4 of 
	 * OIDC specification.
	 */
	private IDTokenClaimsSet prepareIdInfoClaimSet(String userIdentity, List audience, OAuthAuthzContext context, 
			ClaimsSet regularAttributes, Date now)
	{
		AuthenticationRequest request = (AuthenticationRequest) context.getRequest();
		IDTokenClaimsSet idToken = new IDTokenClaimsSet(
				new Issuer(context.getConfig().getIssuerName()), 
				new Subject(userIdentity), 
				audience.stream().filter(a -> a != null).map(Audience::new).collect(Collectors.toList()), 
				new Date(now.getTime() + context.getConfig().getIdTokenValidity()*1000), 
				now);
		ResponseType responseType = request.getResponseType();
		boolean onlyIdTokenRequested = responseType.contains(ID_TOKEN) && responseType.size() == 1; 

		if (onlyIdTokenRequested || context.requestsAttributesInIdToken())
			idToken.putAll(regularAttributes);
		
		if (request.getNonce() != null)
			idToken.setNonce(request.getNonce());
		return idToken;
	}
	
	private void addAccessTokenHashIfNeededToIdToken(Optional idTokenOpt, AccessToken accessToken, 
			JWSAlgorithm jwsAlgorithm, ResponseType responseType, Curve curve)
	{
		if (!idTokenOpt.isPresent())
			return;
		IDTokenClaimsSet idToken = idTokenOpt.get();
		boolean onlyIdTokenRequested = responseType.contains(ID_TOKEN) && responseType.size() == 1; 
		if (!onlyIdTokenRequested)
			idToken.setAccessTokenHash(AccessTokenHash.compute(accessToken, jwsAlgorithm, curve));
	}

	private void addCodeHashIfNeededToIdToken(Optional idTokenOpt, AuthorizationCode code, 
			JWSAlgorithm jwsAlgorithm, ResponseType responseType, Curve curve)
	{
		if (!idTokenOpt.isPresent())
			return;
		IDTokenClaimsSet idToken = idTokenOpt.get();
		if (responseType.contains(ID_TOKEN) && responseType.contains(ResponseType.Value.CODE))
			idToken.setCodeHash(CodeHash.compute(code, jwsAlgorithm, curve));
	}
	
	public static UserInfo prepareUserInfoClaimSet(String userIdentity, Collection attributes)
	{
		UserInfo userInfo = new UserInfo(new Subject(userIdentity));
		
		OAuthAttributeMapper mapper = new DefaultOAuthAttributeMapper();
		
		for (DynamicAttribute dat: attributes)
		{
			Attribute attr = dat.getAttribute();
			if (mapper.isHandled(attr))
			{
				String name = mapper.getJsonKey(attr);
				Object value = mapper.getJsonValue(attr);
				userInfo.setClaim(name, value);
			}
		}
		
		return userInfo;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy