/* * Licensed to Apereo under one or more contributor license * agreements. See the NOTICE file distributed with this work * for additional information regarding copyright ownership. * Apereo 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 the following location: * * 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.jasig.cas.support.oauth; import com.codahale.metrics.annotation.Counted; import com.codahale.metrics.annotation.Metered; import com.codahale.metrics.annotation.Timed; import org.apache.commons.lang3.StringUtils; import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.authentication.AuthenticationException; import org.jasig.cas.authentication.principal.Principal; import org.jasig.cas.authentication.principal.Service; import org.jasig.cas.authentication.principal.SimpleWebApplicationServiceImpl; import org.jasig.cas.services.ServicesManager; import org.jasig.cas.support.oauth.authentication.principal.OAuthCredential; import org.jasig.cas.support.oauth.metadata.ClientMetadata; import org.jasig.cas.support.oauth.metadata.PrincipalMetadata; import org.jasig.cas.support.oauth.personal.PersonalAccessToken; import org.jasig.cas.support.oauth.personal.PersonalAccessTokenManager; import org.jasig.cas.support.oauth.scope.InvalidScopeException; import org.jasig.cas.support.oauth.scope.Scope; import org.jasig.cas.support.oauth.scope.ScopeManager; import org.jasig.cas.support.oauth.services.OAuthRegisteredService; import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.AccessTokenImpl; import org.jasig.cas.support.oauth.token.AuthorizationCode; import org.jasig.cas.support.oauth.token.AuthorizationCodeImpl; import org.jasig.cas.support.oauth.token.InvalidTokenException; import org.jasig.cas.support.oauth.token.RefreshToken; import org.jasig.cas.support.oauth.token.RefreshTokenImpl; import org.jasig.cas.support.oauth.token.Token; import org.jasig.cas.support.oauth.token.TokenType; import org.jasig.cas.support.oauth.token.registry.TokenRegistry; import org.jasig.cas.ticket.ServiceTicket; import org.jasig.cas.ticket.TicketException; import org.jasig.cas.ticket.TicketGrantingTicket; import org.jasig.cas.ticket.registry.TicketRegistry; import org.jasig.cas.util.UniqueTicketIdGenerator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import javax.validation.constraints.NotNull; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * Central OAuth Service implementation. * * @author Michael Haselton * @since 4.1.0 */ public final class CentralOAuthServiceImpl implements CentralOAuthService { /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(CentralOAuthService.class); /** CentralAuthenticationService for requesting tickets as needed. */ @NotNull private final CentralAuthenticationService centralAuthenticationService; /** ServicesManager for verifying service endpoints. */ @NotNull private final ServicesManager servicesManager; /** TokenRegistry for storing and retrieving tokens as needed. */ @NotNull private final TicketRegistry ticketRegistry; /** TokenRegistry for storing and retrieving tokens as needed. */ @NotNull private final TokenRegistry tokenRegistry; /** ScopeManager for storing and retrieving scopes as needed. */ @NotNull private final ScopeManager scopeManager; /** PersonalAccessTokenManager for retrieving personal tokens. */ @NotNull private final PersonalAccessTokenManager personalAccessTokenManager; /** * UniqueTicketIdGenerator to generate ids for AuthorizationCodes * created. */ @NotNull private final UniqueTicketIdGenerator authorizationCodeUniqueIdGenerator; /** * UniqueTicketIdGenerator to generate ids for RefreshTokens * created. */ @NotNull private final UniqueTicketIdGenerator refreshTokenUniqueIdGenerator; /** * UniqueTicketIdGenerator to generate ids for AccessTokens * created. */ @NotNull private final UniqueTicketIdGenerator accessTokenUniqueIdGenerator; /** * Build the central oauth service implementation. * * @param centralAuthenticationService the central authentication service. * @param servicesManager the services manager. * @param ticketRegistry the ticket registry. * @param tokenRegistry the token registry. * @param authorizationCodeUniqueIdGenerator the authorization code unique id generator. * @param refreshTokenUniqueIdGenerator the refresh token unique id generator. * @param accessTokenUniqueIdGenerator the access token unique id generator. * @param scopeManager the scope manager. * @param personalAccessTokenManager the personal access token manager. */ public CentralOAuthServiceImpl(final CentralAuthenticationService centralAuthenticationService, final ServicesManager servicesManager, final TicketRegistry ticketRegistry, final TokenRegistry tokenRegistry, final UniqueTicketIdGenerator authorizationCodeUniqueIdGenerator, final UniqueTicketIdGenerator refreshTokenUniqueIdGenerator, final UniqueTicketIdGenerator accessTokenUniqueIdGenerator, final ScopeManager scopeManager, final PersonalAccessTokenManager personalAccessTokenManager) { this.centralAuthenticationService = centralAuthenticationService; this.servicesManager = servicesManager; this.ticketRegistry = ticketRegistry; this.tokenRegistry = tokenRegistry; this.authorizationCodeUniqueIdGenerator = authorizationCodeUniqueIdGenerator; this.refreshTokenUniqueIdGenerator = refreshTokenUniqueIdGenerator; this.accessTokenUniqueIdGenerator = accessTokenUniqueIdGenerator; this.scopeManager = scopeManager; this.personalAccessTokenManager = personalAccessTokenManager; } @Override public OAuthRegisteredService getRegisteredService(final String clientId) { return OAuthUtils.getRegisteredOAuthService(servicesManager, clientId); } @Override public AuthorizationCode grantAuthorizationCode(final TokenType type, final String clientId, final String ticketGrantingTicketId, final String redirectUri, final Set<String> scopes) throws TicketException { final Service service = new SimpleWebApplicationServiceImpl(redirectUri); final ServiceTicket serviceTicket = centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service); final AuthorizationCodeImpl authorizationCode = new AuthorizationCodeImpl( this.authorizationCodeUniqueIdGenerator.getNewTicketId(AuthorizationCode.PREFIX), type, clientId, serviceTicket.getGrantingTicket().getAuthentication().getPrincipal().getId(), serviceTicket, scopes); LOGGER.debug("{} : {}", OAuthConstants.AUTHORIZATION_CODE, authorizationCode); this.tokenRegistry.addToken(authorizationCode); return authorizationCode; } @Override public RefreshToken grantOfflineRefreshToken(final AuthorizationCode authorizationCode, final String redirectUri) throws InvalidTokenException { final Principal principal = authorizationCode.getServiceTicket().getGrantingTicket().getAuthentication().getPrincipal(); final OAuthCredential credential = new OAuthCredential(principal.getId(), principal.getAttributes(), TokenType.OFFLINE); final TicketGrantingTicket ticketGrantingTicket; try { ticketGrantingTicket = centralAuthenticationService.createTicketGrantingTicket(credential); } catch (final AuthenticationException | TicketException e) { throw new InvalidTokenException(authorizationCode.getId()); } final RefreshToken refreshToken = new RefreshTokenImpl( refreshTokenUniqueIdGenerator.getNewTicketId(RefreshToken.PREFIX), authorizationCode.getClientId(), authorizationCode.getServiceTicket().getGrantingTicket().getAuthentication().getPrincipal().getId(), ticketGrantingTicket, authorizationCode.getServiceTicket().getService(), authorizationCode.getScopes()); LOGGER.debug("Offline {} : {}", OAuthConstants.REFRESH_TOKEN, refreshToken); // remove the service ticket, doing so will cascade and remove the authorization code token ticketRegistry.deleteTicket(authorizationCode.getTicket().getId()); tokenRegistry.addToken(refreshToken); return refreshToken; } @Override public AccessToken grantCASAccessToken(final TicketGrantingTicket ticketGrantingTicket, final Service service) throws TicketException { final AccessToken accessToken = new AccessTokenImpl( accessTokenUniqueIdGenerator.getNewTicketId(AccessToken.PREFIX), TokenType.CAS, null, ticketGrantingTicket.getAuthentication().getPrincipal().getId(), ticketGrantingTicket, service, null, scopeManager.getCASScopes()); LOGGER.debug("CAS {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); tokenRegistry.addToken(accessToken); return accessToken; } @Override public AccessToken grantPersonalAccessToken(final PersonalAccessToken personalAccessToken) throws InvalidTokenException { final OAuthCredential credential = new OAuthCredential(personalAccessToken.getPrincipalId(), TokenType.PERSONAL); final TicketGrantingTicket ticketGrantingTicket; try { ticketGrantingTicket = centralAuthenticationService.createTicketGrantingTicket(credential); } catch (final AuthenticationException | TicketException e) { throw new InvalidTokenException(personalAccessToken.getId()); } final AccessToken accessToken = new AccessTokenImpl( personalAccessToken.getId(), TokenType.PERSONAL, null, personalAccessToken.getPrincipalId(), ticketGrantingTicket, null, null, personalAccessToken.getScopes()); LOGGER.debug("Personal {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); tokenRegistry.addToken(accessToken); return accessToken; } @Override public AccessToken grantOfflineAccessToken(final RefreshToken refreshToken) throws InvalidTokenException { final ServiceTicket serviceTicket; try { serviceTicket = centralAuthenticationService.grantServiceTicket(refreshToken.getTicketGrantingTicket().getId(), refreshToken.getService()); } catch (final TicketException e) { throw new InvalidTokenException(refreshToken.getId()); } final AccessToken accessToken = new AccessTokenImpl( accessTokenUniqueIdGenerator.getNewTicketId(AccessToken.PREFIX), TokenType.OFFLINE, refreshToken.getClientId(), refreshToken.getTicketGrantingTicket().getAuthentication().getPrincipal().getId(), null, null, serviceTicket, refreshToken.getScopes()); LOGGER.debug("Offline {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); tokenRegistry.addToken(accessToken); return accessToken; } @Override public AccessToken grantOnlineAccessToken(final AuthorizationCode authorizationCode) throws InvalidTokenException { final Principal principal = authorizationCode.getServiceTicket().getGrantingTicket().getAuthentication().getPrincipal(); final OAuthCredential credential = new OAuthCredential(principal.getId(), principal.getAttributes(), TokenType.ONLINE); final TicketGrantingTicket ticketGrantingTicket; try { ticketGrantingTicket = centralAuthenticationService.createTicketGrantingTicket(credential); } catch (final AuthenticationException | TicketException e) { throw new InvalidTokenException(authorizationCode.getId()); } final AccessToken accessToken = new AccessTokenImpl( accessTokenUniqueIdGenerator.getNewTicketId(AccessToken.PREFIX), TokenType.ONLINE, authorizationCode.getClientId(), authorizationCode.getServiceTicket().getGrantingTicket().getAuthentication().getPrincipal().getId(), ticketGrantingTicket, authorizationCode.getServiceTicket().getService(), null, authorizationCode.getScopes()); LOGGER.debug("Online {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); // remove the service ticket, doing so will cascade and remove the authorization code token ticketRegistry.deleteTicket(authorizationCode.getTicket().getId()); tokenRegistry.addToken(accessToken); return accessToken; } @Override public Boolean revokeToken(final Token token) { return ticketRegistry.deleteTicket(token.getTicket().getId()); } @Override public Boolean revokeClientTokens(final String clientId, final String clientSecret) { final OAuthRegisteredService service = getRegisteredService(clientId); if (service == null) { LOGGER.error("OAuth Registered Service could not be found for clientId : {}", clientId); return Boolean.FALSE; } if (!service.getClientSecret().equals(clientSecret)) { LOGGER.error("Invalid client secret"); return Boolean.FALSE; } final Collection<RefreshToken> refreshTokens = tokenRegistry.getClientTokens(clientId, RefreshToken.class); for (final RefreshToken token : refreshTokens) { LOGGER.debug("Revoking refresh token : {}", token.getId()); ticketRegistry.deleteTicket(token.getTicket().getId()); } final Collection<AccessToken> accessTokens = tokenRegistry.getClientTokens(clientId, AccessToken.class); for (final AccessToken token : accessTokens) { LOGGER.debug("Revoking access token : {}", token.getId()); ticketRegistry.deleteTicket(token.getTicket().getId()); } return Boolean.TRUE; } @Override public Boolean revokeClientPrincipalTokens(final AccessToken accessToken, final String clientId) { final String targetClientId; if (accessToken.getType() == TokenType.CAS) { // Only CAS Tokens are allowed to specify the client id for revocation. if (StringUtils.isBlank(clientId)) { LOGGER.warn("CAS Token used for revocation, Client ID must be specified"); return Boolean.FALSE; } targetClientId = clientId; } else { if (!accessToken.getClientId().equals(clientId)) { LOGGER.warn("Access Token's Client ID and specified Client ID must match"); return Boolean.FALSE; } targetClientId = accessToken.getClientId(); } final Collection<RefreshToken> refreshTokens = tokenRegistry.getClientPrincipalTokens(targetClientId, accessToken.getPrincipalId(), RefreshToken.class); for (final RefreshToken token : refreshTokens) { LOGGER.debug("Revoking refresh token : {}", token.getId()); ticketRegistry.deleteTicket(token.getTicketGrantingTicket().getId()); } final Collection<AccessToken> accessTokens = tokenRegistry.getClientPrincipalTokens(targetClientId, accessToken.getPrincipalId(), TokenType.ONLINE, AccessToken.class); for (final AccessToken token : accessTokens) { LOGGER.debug("Revoking access token : {}", token.getId()); ticketRegistry.deleteTicket(token.getTicketGrantingTicket().getId()); } return Boolean.TRUE; } @Override public ClientMetadata getClientMetadata(final String clientId, final String clientSecret) { final OAuthRegisteredService service = getRegisteredService(clientId); if (service == null) { LOGGER.error("OAuth Registered Service could not be found for clientId : {}", clientId); return null; } if (!service.getClientSecret().equals(clientSecret)) { LOGGER.error("Invalid Client Secret specified for Client ID [{}]]", clientId); return null; } return new ClientMetadata(service.getClientId(), service.getName(), service.getDescription(), tokenRegistry.getPrincipalCount(clientId)); } @Override public Collection<PrincipalMetadata> getPrincipalMetadata(final AccessToken accessToken) throws InvalidTokenException { if (accessToken.getType() != TokenType.CAS) { // Only CAS Tokens are allowed to access principal metadata. LOGGER.warn("Principal Metadata can only be accessed with an Access Token of type CAS"); throw new InvalidTokenException(accessToken.getId()); } final Map<String, PrincipalMetadata> metadata = new HashMap<>(); for (final Token token : tokenRegistry.getPrincipalTokens(accessToken.getPrincipalId(), RefreshToken.class)) { final PrincipalMetadata serviceDetail; if (!metadata.containsKey(token.getClientId())) { final OAuthRegisteredService service = getRegisteredService(token.getClientId()); serviceDetail = new PrincipalMetadata(service.getClientId(), service.getName(), service.getDescription()); metadata.put(token.getClientId(), serviceDetail); } else { serviceDetail = metadata.get(token.getClientId()); } serviceDetail.getScopes().addAll(token.getScopes()); } for (final Token token : tokenRegistry.getPrincipalTokens(accessToken.getPrincipalId(), AccessToken.class)) { final PrincipalMetadata serviceDetail; if (!metadata.containsKey(token.getClientId())) { final OAuthRegisteredService service = getRegisteredService(token.getClientId()); serviceDetail = new PrincipalMetadata(service.getClientId(), service.getName(), service.getDescription()); metadata.put(token.getClientId(), serviceDetail); } else { serviceDetail = metadata.get(token.getClientId()); } serviceDetail.getScopes().addAll(token.getScopes()); } return metadata.values(); } @Override public Boolean isRefreshToken(final String clientId, final String principalId, final Set<String> scopes) { return tokenRegistry.isToken(clientId, principalId, scopes, RefreshToken.class); } @Override public Boolean isAccessToken(final TokenType type, final String clientId, final String principalId, final Set<String> scopes) { return tokenRegistry.isToken(type, clientId, principalId, scopes, AccessToken.class); } @Override public Token getToken(final String tokenId) throws InvalidTokenException { Assert.notNull(tokenId, "tokenId cannot be null"); if (tokenId.startsWith(AuthorizationCode.PREFIX)) { return getToken(tokenId, AuthorizationCode.class); } else if (tokenId.startsWith(RefreshToken.PREFIX)) { return getToken(tokenId, RefreshToken.class); } return getToken(tokenId, AccessToken.class); } @Override @Timed(name = "GET_TOKEN_TIMER") @Metered(name = "GET_TOKEN_METER") @Counted(name="GET_TOKEN_COUNTER", monotonic=true) public <T extends Token> T getToken(final String tokenId, final Class<T> clazz) throws InvalidTokenException { Assert.notNull(tokenId, "tokenId cannot be null"); final T token = this.tokenRegistry.getToken(tokenId, clazz); if (token == null) { LOGGER.error("Token [{}] by type [{}] cannot be found in the token registry.", tokenId, clazz.getSimpleName()); throw new InvalidTokenException(tokenId); } if (token.getTicket().isExpired()) { // cleanup the expired ticket and token. ticketRegistry.deleteTicket(token.getTicket().getId()); LOGGER.error("Token [{}] ticket [{}] is expired.", tokenId, token.getTicket().getId()); throw new InvalidTokenException(tokenId); } return token; } @Override public PersonalAccessToken getPersonalAccessToken(final String tokenId) { Assert.notNull(tokenId, "tokenId cannot be null"); if (personalAccessTokenManager != null) { return personalAccessTokenManager.getToken(tokenId); } return null; } @Override public Map<String, Scope> getScopes(final Set<String> scopeSet) throws InvalidScopeException { Assert.notNull(scopeSet, "scopeSet cannot be null"); final Map<String, Scope> scopeMap = new HashMap<>(); for (final String scope : scopeSet) { final Scope oAuthScope = scopeManager.getScope(scope); if (oAuthScope == null) { LOGGER.error("Could not find requested scope: {}", scope); throw new InvalidScopeException(scope); } scopeMap.put(oAuthScope.getName(), oAuthScope); } for (final Scope defaultScope : scopeManager.getDefaults()) { scopeMap.put(defaultScope.getName(), defaultScope); } return scopeMap; } }