package org.apereo.cas.support.oauth.web.endpoints; import org.apache.commons.lang3.StringUtils; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URIBuilder; import org.apereo.cas.authentication.Authentication; import org.apereo.cas.authentication.PrincipalException; import org.apereo.cas.authentication.principal.PrincipalFactory; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.authentication.principal.ServiceFactory; import org.apereo.cas.authentication.principal.WebApplicationService; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.services.RegisteredServiceAccessStrategyUtils; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.services.UnauthorizedServiceException; import org.apereo.cas.support.oauth.OAuth20Constants; import org.apereo.cas.support.oauth.OAuth20ResponseTypes; import org.apereo.cas.support.oauth.authenticator.OAuth20CasAuthenticationBuilder; import org.apereo.cas.support.oauth.profile.OAuth20ProfileScopeToAttributesFilter; import org.apereo.cas.support.oauth.services.OAuthRegisteredService; import org.apereo.cas.support.oauth.util.OAuth20Utils; import org.apereo.cas.support.oauth.validator.OAuth20Validator; import org.apereo.cas.support.oauth.web.response.accesstoken.ext.AccessTokenRequestDataHolder; import org.apereo.cas.support.oauth.web.views.ConsentApprovalViewResolver; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.accesstoken.AccessToken; import org.apereo.cas.ticket.accesstoken.AccessTokenFactory; import org.apereo.cas.ticket.code.OAuthCode; import org.apereo.cas.ticket.code.OAuthCodeFactory; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.util.EncodingUtils; import org.apereo.cas.web.support.CookieRetrievingCookieGenerator; import org.apereo.cas.web.support.CookieUtils; import org.apereo.cas.web.support.WebUtils; import org.pac4j.core.context.J2EContext; import org.pac4j.core.profile.CommonProfile; import org.pac4j.core.profile.ProfileManager; import org.pac4j.core.profile.UserProfile; import org.pac4j.core.util.CommonHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Stream; /** * This controller is in charge of responding to the authorize call in OAuth v2 protocol. * This url is protected by a CAS authentication. It returns an OAuth code or directly an access token. * * @author Jerome Leleu * @since 3.5.0 */ public class OAuth20AuthorizeEndpointController extends BaseOAuth20Controller { private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20AuthorizeEndpointController.class); /** * The code factory instance. */ protected OAuthCodeFactory oAuthCodeFactory; /** * The Consent approval view resolver. */ protected final ConsentApprovalViewResolver consentApprovalViewResolver; /** * The Authentication builder. */ protected final OAuth20CasAuthenticationBuilder authenticationBuilder; public OAuth20AuthorizeEndpointController(final ServicesManager servicesManager, final TicketRegistry ticketRegistry, final OAuth20Validator validator, final AccessTokenFactory accessTokenFactory, final PrincipalFactory principalFactory, final ServiceFactory<WebApplicationService> webApplicationServiceServiceFactory, final OAuthCodeFactory oAuthCodeFactory, final ConsentApprovalViewResolver consentApprovalViewResolver, final OAuth20ProfileScopeToAttributesFilter scopeToAttributesFilter, final CasConfigurationProperties casProperties, final CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator, final OAuth20CasAuthenticationBuilder authenticationBuilder) { super(servicesManager, ticketRegistry, validator, accessTokenFactory, principalFactory, webApplicationServiceServiceFactory, scopeToAttributesFilter, casProperties, ticketGrantingTicketCookieGenerator); this.oAuthCodeFactory = oAuthCodeFactory; this.consentApprovalViewResolver = consentApprovalViewResolver; this.authenticationBuilder = authenticationBuilder; } /** * Handle request internal model and view. * * @param request the request * @param response the response * @return the model and view * @throws Exception the exception */ @GetMapping(path = OAuth20Constants.BASE_OAUTH20_URL + '/' + OAuth20Constants.AUTHORIZE_URL) public ModelAndView handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws Exception { final J2EContext context = WebUtils.getPac4jJ2EContext(request, response); final ProfileManager manager = WebUtils.getPac4jProfileManager(request, response); if (!verifyAuthorizeRequest(request) || !isRequestAuthenticated(manager, context)) { LOGGER.error("Authorize request verification failed"); return OAuth20Utils.produceUnauthorizedErrorView(); } final String clientId = context.getRequestParameter(OAuth20Constants.CLIENT_ID); final OAuthRegisteredService registeredService = getRegisteredServiceByClientId(clientId); try { RegisteredServiceAccessStrategyUtils.ensureServiceAccessIsAllowed(clientId, registeredService); } catch (final Exception e) { LOGGER.error(e.getMessage(), e); return OAuth20Utils.produceUnauthorizedErrorView(); } final ModelAndView mv = this.consentApprovalViewResolver.resolve(context, registeredService); if (!mv.isEmpty() && mv.hasView()) { return mv; } return redirectToCallbackRedirectUrl(manager, registeredService, context, clientId); } /** * Gets registered service by client id. * * @param clientId the client id * @return the registered service by client id */ protected OAuthRegisteredService getRegisteredServiceByClientId(final String clientId) { return OAuth20Utils.getRegisteredOAuthService(this.servicesManager, clientId); } private static boolean isRequestAuthenticated(final ProfileManager manager, final J2EContext context) { final Optional<CommonProfile> opt = manager.get(true); return opt.isPresent(); } /** * Redirect to callback redirect url model and view. * * @param manager the manager * @param registeredService the registered service * @param context the context * @param clientId the client id * @return the model and view * @throws Exception the exception */ protected ModelAndView redirectToCallbackRedirectUrl(final ProfileManager manager, final OAuthRegisteredService registeredService, final J2EContext context, final String clientId) throws Exception { final Optional<UserProfile> profile = manager.get(true); if (profile == null || !profile.isPresent()) { LOGGER.error("Unexpected null profile from profile manager. Request is not fully authenticated."); return OAuth20Utils.produceUnauthorizedErrorView(); } final Service service = this.authenticationBuilder.buildService(registeredService, context, false); LOGGER.debug("Created service [{}] based on registered service [{}]", service, registeredService); final Authentication authentication = this.authenticationBuilder.build(profile.get(), registeredService, context, service); LOGGER.debug("Created OAuth authentication [{}] for service [{}]", service, authentication); try { RegisteredServiceAccessStrategyUtils.ensurePrincipalAccessIsAllowedForService(service, registeredService, authentication); } catch (final UnauthorizedServiceException | PrincipalException e) { LOGGER.error(e.getMessage(), e); return OAuth20Utils.produceUnauthorizedErrorView(); } final String redirectUri = context.getRequestParameter(OAuth20Constants.REDIRECT_URI); LOGGER.debug("Authorize request verification successful for client [{}] with redirect uri [{}]", clientId, redirectUri); final String responseType = context.getRequestParameter(OAuth20Constants.RESPONSE_TYPE); final TicketGrantingTicket ticketGrantingTicket = CookieUtils.getTicketGrantingTicketFromRequest( ticketGrantingTicketCookieGenerator, this.ticketRegistry, context.getRequest()); final String callbackUrl; if (OAuth20Utils.isResponseType(responseType, OAuth20ResponseTypes.CODE)) { callbackUrl = buildCallbackUrlForAuthorizationCodeResponseType(authentication, service, redirectUri, ticketGrantingTicket); } else if (OAuth20Utils.isResponseType(responseType, OAuth20ResponseTypes.TOKEN)) { final AccessTokenRequestDataHolder holder = new AccessTokenRequestDataHolder(service, authentication, registeredService, ticketGrantingTicket); callbackUrl = buildCallbackUrlForImplicitTokenResponseType(holder, redirectUri); } else { callbackUrl = buildCallbackUrlForTokenResponseType(context, authentication, service, redirectUri, responseType, clientId); } LOGGER.debug("Callback URL to redirect: [{}]", callbackUrl); if (StringUtils.isBlank(callbackUrl)) { return OAuth20Utils.produceUnauthorizedErrorView(); } return OAuth20Utils.redirectTo(callbackUrl); } /** * Build callback url for token response type string. * * @param context the context * @param authentication the authentication * @param service the service * @param redirectUri the redirect uri * @param responseType the response type * @param clientId the client id * @return the callback url */ protected String buildCallbackUrlForTokenResponseType(final J2EContext context, final Authentication authentication, final Service service, final String redirectUri, final String responseType, final String clientId) { return null; } private String buildCallbackUrlForImplicitTokenResponseType(final AccessTokenRequestDataHolder holder, final String redirectUri) throws Exception { final AccessToken accessToken = generateAccessToken(holder); LOGGER.debug("Generated OAuth access token: [{}]", accessToken); return buildCallbackUrlResponseType(holder.getAuthentication(), holder.getService(), redirectUri, accessToken, Collections.emptyList()); } /** * Build callback url response type string. * * @param authentication the authentication * @param service the service * @param redirectUri the redirect uri * @param accessToken the access token * @param params the params * @return the string * @throws Exception the exception */ protected String buildCallbackUrlResponseType(final Authentication authentication, final Service service, final String redirectUri, final AccessToken accessToken, final List<NameValuePair> params) throws Exception { final String state = authentication.getAttributes().get(OAuth20Constants.STATE).toString(); final String nonce = authentication.getAttributes().get(OAuth20Constants.NONCE).toString(); final URIBuilder builder = new URIBuilder(redirectUri); final StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(OAuth20Constants.ACCESS_TOKEN) .append('=') .append(accessToken.getId()) .append('&') .append(OAuth20Constants.TOKEN_TYPE) .append('=') .append(OAuth20Constants.TOKEN_TYPE_BEARER) .append('&') .append(OAuth20Constants.EXPIRES_IN) .append('=') .append(casProperties.getTicket().getTgt().getTimeToKillInSeconds()); params.forEach(p -> stringBuilder.append('&') .append(p.getName()) .append('=') .append(p.getValue())); if (StringUtils.isNotBlank(state)) { stringBuilder.append('&') .append(OAuth20Constants.STATE) .append('=') .append(EncodingUtils.urlEncode(state)); } if (StringUtils.isNotBlank(nonce)) { stringBuilder.append('&') .append(OAuth20Constants.NONCE) .append('=') .append(EncodingUtils.urlEncode(nonce)); } builder.setFragment(stringBuilder.toString()); final String url = builder.toString(); return url; } private String buildCallbackUrlForAuthorizationCodeResponseType(final Authentication authentication, final Service service, final String redirectUri, final TicketGrantingTicket ticketGrantingTicket) { final OAuthCode code = this.oAuthCodeFactory.create(service, authentication, ticketGrantingTicket); LOGGER.debug("Generated OAuth code: [{}]", code); this.ticketRegistry.addTicket(code); final String state = authentication.getAttributes().get(OAuth20Constants.STATE).toString(); final String nonce = authentication.getAttributes().get(OAuth20Constants.NONCE).toString(); String callbackUrl = redirectUri; callbackUrl = CommonHelper.addParameter(callbackUrl, OAuth20Constants.CODE, code.getId()); if (StringUtils.isNotBlank(state)) { callbackUrl = CommonHelper.addParameter(callbackUrl, OAuth20Constants.STATE, state); } if (StringUtils.isNotBlank(nonce)) { callbackUrl = CommonHelper.addParameter(callbackUrl, OAuth20Constants.NONCE, nonce); } return callbackUrl; } /** * Verify the authorize request. * * @param request the HTTP request * @return whether the authorize request is valid */ private boolean verifyAuthorizeRequest(final HttpServletRequest request) { final boolean checkParameterExist = this.validator.checkParameterExist(request, OAuth20Constants.CLIENT_ID) && this.validator.checkParameterExist(request, OAuth20Constants.REDIRECT_URI) && this.validator.checkParameterExist(request, OAuth20Constants.RESPONSE_TYPE); final String responseType = request.getParameter(OAuth20Constants.RESPONSE_TYPE); final String clientId = request.getParameter(OAuth20Constants.CLIENT_ID); final String redirectUri = request.getParameter(OAuth20Constants.REDIRECT_URI); final OAuthRegisteredService registeredService = getRegisteredServiceByClientId(clientId); return checkParameterExist && checkResponseTypes(responseType, OAuth20ResponseTypes.values()) && this.validator.checkServiceValid(registeredService) && this.validator.checkCallbackValid(registeredService, redirectUri); } /** * Check the response type against expected response types. * * @param type the current response type * @param expectedTypes the expected response types * @return whether the response type is supported */ private static boolean checkResponseTypes(final String type, final OAuth20ResponseTypes... expectedTypes) { LOGGER.debug("Response type: [{}]", type); final boolean checked = Stream.of(expectedTypes).anyMatch(t -> OAuth20Utils.isResponseType(type, t)); if (!checked) { LOGGER.error("Unsupported response type: [{}]", type); } return checked; } }