package org.apereo.cas.support.oauth.web.endpoints; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.principal.PrincipalFactory; 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.ServicesManager; import org.apereo.cas.support.oauth.OAuth20Constants; import org.apereo.cas.support.oauth.profile.OAuth20ProfileScopeToAttributesFilter; import org.apereo.cas.support.oauth.util.OAuth20Utils; import org.apereo.cas.support.oauth.validator.OAuth20Validator; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.TicketState; import org.apereo.cas.ticket.accesstoken.AccessToken; import org.apereo.cas.ticket.accesstoken.AccessTokenFactory; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.web.support.CookieRetrievingCookieGenerator; import org.pac4j.core.context.HttpConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * This controller returns a profile for the authenticated user * (identifier + attributes), found with the access token. * * @author Jerome Leleu * @since 3.5.0 */ public class OAuth20UserProfileControllerController extends BaseOAuth20Controller { private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20UserProfileControllerController.class); private static final String ID = "id"; private static final String ATTRIBUTES = "attributes"; public OAuth20UserProfileControllerController(final ServicesManager servicesManager, final TicketRegistry ticketRegistry, final OAuth20Validator validator, final AccessTokenFactory accessTokenFactory, final PrincipalFactory principalFactory, final ServiceFactory<WebApplicationService> webApplicationServiceServiceFactory, final OAuth20ProfileScopeToAttributesFilter scopeToAttributesFilter, final CasConfigurationProperties casProperties, final CookieRetrievingCookieGenerator cookieGenerator) { super(servicesManager, ticketRegistry, validator, accessTokenFactory, principalFactory, webApplicationServiceServiceFactory, scopeToAttributesFilter, casProperties, cookieGenerator); } /** * Handle request internal response entity. * * @param request the request * @param response the response * @return the response entity * @throws Exception the exception */ @GetMapping(path = OAuth20Constants.BASE_OAUTH20_URL + '/' + OAuth20Constants.PROFILE_URL, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<String> handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws Exception { response.setContentType(MediaType.APPLICATION_JSON_VALUE); final String accessToken = getAccessTokenFromRequest(request); if (StringUtils.isBlank(accessToken)) { LOGGER.error("Missing [{}]", OAuth20Constants.ACCESS_TOKEN); return buildUnauthorizedResponseEntity(OAuth20Constants.MISSING_ACCESS_TOKEN); } final AccessToken accessTokenTicket = this.ticketRegistry.getTicket(accessToken, AccessToken.class); if (accessTokenTicket == null || accessTokenTicket.isExpired()) { LOGGER.error("Expired/Missing access token: [{}]", accessToken); return buildUnauthorizedResponseEntity(OAuth20Constants.EXPIRED_ACCESS_TOKEN); } final TicketGrantingTicket ticketGrantingTicket = accessTokenTicket.getGrantingTicket(); if (ticketGrantingTicket == null || ticketGrantingTicket.isExpired()) { LOGGER.error("Ticket granting ticket [{}] parenting access token [{}] has expired or is not found", ticketGrantingTicket, accessTokenTicket); this.ticketRegistry.deleteTicket(accessToken); return buildUnauthorizedResponseEntity(OAuth20Constants.EXPIRED_ACCESS_TOKEN); } updateAccessTokenUsage(accessTokenTicket); final Map<String, Object> map = writeOutProfileResponse(accessTokenTicket); final String value = OAuth20Utils.jsonify(map); LOGGER.debug("Final user profile is [{}]", value); return new ResponseEntity<>(value, HttpStatus.OK); } private void updateAccessTokenUsage(final AccessToken accessTokenTicket) { final TicketState accessTokenState = TicketState.class.cast(accessTokenTicket); accessTokenState.update(); if (accessTokenTicket.isExpired()) { this.ticketRegistry.deleteTicket(accessTokenTicket.getId()); } else { this.ticketRegistry.updateTicket(accessTokenTicket); } } /** * Gets access token from request. * * @param request the request * @return the access token from request */ protected String getAccessTokenFromRequest(final HttpServletRequest request) { String accessToken = request.getParameter(OAuth20Constants.ACCESS_TOKEN); if (StringUtils.isBlank(accessToken)) { final String authHeader = request.getHeader(HttpConstants.AUTHORIZATION_HEADER); if (StringUtils.isNotBlank(authHeader) && authHeader.toLowerCase().startsWith(OAuth20Constants.BEARER_TOKEN.toLowerCase() + ' ')) { accessToken = authHeader.substring(OAuth20Constants.BEARER_TOKEN.length() + 1); } } LOGGER.debug("[{}]: [{}]", OAuth20Constants.ACCESS_TOKEN, accessToken); return accessToken; } /** * Write out profile response. * * @param accessToken the access token * @return the linked multi value map * @throws IOException the io exception */ protected Map<String, Object> writeOutProfileResponse(final AccessToken accessToken) throws IOException { final Principal principal = accessToken.getAuthentication().getPrincipal(); LOGGER.debug("Preparing user profile response based on CAS principal [{}]", principal); final Map<String, Object> map = new HashMap<>(); map.put(ID, principal.getId()); map.put(ATTRIBUTES, principal.getAttributes()); return map; } /** * Build unauthorized response entity. * * @param code the code * @return the response entity */ private static ResponseEntity buildUnauthorizedResponseEntity(final String code) { final LinkedMultiValueMap<String, String> map = new LinkedMultiValueMap<>(1); map.add(OAuth20Constants.ERROR, code); final String value = OAuth20Utils.jsonify(map); return new ResponseEntity<>(value, HttpStatus.UNAUTHORIZED); } }