/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF 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.apache.cxf.rs.security.oauth2.provider; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.ws.rs.core.MultivaluedMap; import org.apache.cxf.jaxrs.ext.MessageContext; import org.apache.cxf.rs.security.jose.common.JoseConstants; import org.apache.cxf.rs.security.jose.jwt.JwtClaims; import org.apache.cxf.rs.security.jose.jwt.JwtConstants; import org.apache.cxf.rs.security.jose.jwt.JwtToken; import org.apache.cxf.rs.security.oauth2.common.AccessTokenRegistration; import org.apache.cxf.rs.security.oauth2.common.Client; import org.apache.cxf.rs.security.oauth2.common.OAuthPermission; import org.apache.cxf.rs.security.oauth2.common.ServerAccessToken; import org.apache.cxf.rs.security.oauth2.common.UserSubject; import org.apache.cxf.rs.security.oauth2.tokens.bearer.BearerAccessToken; import org.apache.cxf.rs.security.oauth2.tokens.refresh.RefreshToken; import org.apache.cxf.rs.security.oauth2.utils.JwtTokenUtils; import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants; import org.apache.cxf.rs.security.oauth2.utils.OAuthUtils; public abstract class AbstractOAuthDataProvider implements OAuthDataProvider, ClientRegistrationProvider { private long accessTokenLifetime = 3600L; private long refreshTokenLifetime; // refresh tokens are eternal by default private boolean recycleRefreshTokens = true; private Map<String, OAuthPermission> permissionMap = new HashMap<>(); private MessageContext messageContext; private List<String> defaultScopes; private List<String> requiredScopes; private List<String> invisibleToClientScopes; private boolean supportPreauthorizedTokens; private boolean useJwtFormatForAccessTokens; private OAuthJoseJwtProducer jwtAccessTokenProducer; private Map<String, String> jwtAccessTokenClaimMap; private ProviderAuthenticationStrategy authenticationStrategy; protected AbstractOAuthDataProvider() { } @Override public ServerAccessToken createAccessToken(AccessTokenRegistration reg) throws OAuthServiceException { ServerAccessToken at = doCreateAccessToken(reg); saveAccessToken(at); if (isRefreshTokenSupported(reg.getApprovedScope())) { createNewRefreshToken(at); } return at; } protected ServerAccessToken doCreateAccessToken(AccessTokenRegistration atReg) { ServerAccessToken at = createNewAccessToken(atReg.getClient(), atReg.getSubject()); at.setAudiences(atReg.getAudiences()); at.setGrantType(atReg.getGrantType()); List<String> theScopes = atReg.getApprovedScope(); List<OAuthPermission> thePermissions = convertScopeToPermissions(atReg.getClient(), theScopes); at.setScopes(thePermissions); at.setSubject(atReg.getSubject()); at.setClientCodeVerifier(atReg.getClientCodeVerifier()); at.setNonce(atReg.getNonce()); at.setResponseType(atReg.getResponseType()); at.setGrantCode(atReg.getGrantCode()); at.getExtraProperties().putAll(atReg.getExtraProperties()); if (messageContext != null) { String certCnf = (String)messageContext.get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256); if (certCnf != null) { // At a later stage we will likely introduce a dedicate Confirmation bean (as it is used in POP etc) at.getExtraProperties().put(JoseConstants.HEADER_X509_THUMBPRINT_SHA256, certCnf); } } if (isUseJwtFormatForAccessTokens()) { JwtClaims claims = createJwtAccessToken(at); String jose = processJwtAccessToken(claims); at.setTokenKey(jose); } return at; } protected JwtClaims createJwtAccessToken(ServerAccessToken at) { JwtClaims claims = new JwtClaims(); claims.setTokenId(at.getTokenKey()); // 'client_id' or 'cid', default client_id String clientIdClaimName = JwtTokenUtils.getClaimName(OAuthConstants.CLIENT_ID, OAuthConstants.CLIENT_ID, getJwtAccessTokenClaimMap()); claims.setClaim(clientIdClaimName, at.getClient().getClientId()); claims.setIssuedAt(at.getIssuedAt()); if (at.getExpiresIn() > 0) { claims.setExpiryTime(at.getIssuedAt() + at.getExpiresIn()); } UserSubject userSubject = at.getSubject(); if (userSubject != null) { if (userSubject.getId() != null) { claims.setSubject(userSubject.getId()); } // 'username' by default to be consistent with the token introspection response final String usernameProp = "username"; String usernameClaimName = JwtTokenUtils.getClaimName(usernameProp, usernameProp, getJwtAccessTokenClaimMap()); claims.setClaim(usernameClaimName, userSubject.getLogin()); } if (at.getIssuer() != null) { claims.setIssuer(at.getIssuer()); } if (!at.getScopes().isEmpty()) { claims.setClaim(OAuthConstants.SCOPE, OAuthUtils.convertPermissionsToScopeList(at.getScopes())); } // OAuth2 resource indicators (resource server audience) if (!at.getAudiences().isEmpty()) { List<String> resourceAudiences = at.getAudiences(); if (resourceAudiences.size() == 1) { claims.setAudience(resourceAudiences.get(0)); } else { claims.setAudiences(resourceAudiences); } } if (!at.getExtraProperties().isEmpty()) { Map<String, String> actualExtraProps = new HashMap<String, String>(); for (Map.Entry<String, String> entry : at.getExtraProperties().entrySet()) { if (JoseConstants.HEADER_X509_THUMBPRINT_SHA256.equals(entry.getKey())) { claims.setClaim(JwtConstants.CLAIM_CONFIRMATION, Collections.singletonMap(JoseConstants.HEADER_X509_THUMBPRINT_SHA256, entry.getValue())); } else { actualExtraProps.put(entry.getKey(), entry.getValue()); } } claims.setClaim("extra_properties", actualExtraProps); } // Can be used to check at RS/etc which grant was used to get this token issued if (at.getGrantType() != null) { claims.setClaim(OAuthConstants.GRANT_TYPE, at.getGrantType()); } // Can be used to check the original code grant value which was removed from the storage // (and is no longer valid) when this token was issued; relevant only if the authorization // code flow was used if (at.getGrantCode() != null) { claims.setClaim(OAuthConstants.AUTHORIZATION_CODE_GRANT, at.getGrantCode()); } // Can be used to link the clients (especially public ones) to this token // to have a knowledge which client instance is using this token - might be handy at the RS/etc if (at.getClientCodeVerifier() != null) { claims.setClaim(OAuthConstants.AUTHORIZATION_CODE_VERIFIER, at.getClientCodeVerifier()); } if (at.getNonce() != null) { claims.setClaim(OAuthConstants.NONCE, at.getNonce()); } return claims; } protected ServerAccessToken createNewAccessToken(Client client, UserSubject userSub) { return new BearerAccessToken(client, accessTokenLifetime); } @Override public ServerAccessToken refreshAccessToken(Client client, String refreshTokenKey, List<String> restrictedScopes) throws OAuthServiceException { RefreshToken currentRefreshToken = recycleRefreshTokens ? revokeRefreshToken(refreshTokenKey) : getRefreshToken(refreshTokenKey); if (currentRefreshToken == null) { throw new OAuthServiceException(OAuthConstants.ACCESS_DENIED); } if (OAuthUtils.isExpired(currentRefreshToken.getIssuedAt(), currentRefreshToken.getExpiresIn())) { if (!recycleRefreshTokens) { revokeRefreshToken(refreshTokenKey); } throw new OAuthServiceException(OAuthConstants.ACCESS_DENIED); } if (recycleRefreshTokens) { revokeAccessTokens(currentRefreshToken); } ServerAccessToken at = doRefreshAccessToken(client, currentRefreshToken, restrictedScopes); saveAccessToken(at); if (recycleRefreshTokens) { createNewRefreshToken(at); } else { updateRefreshToken(currentRefreshToken, at); } return at; } @Override public void revokeToken(Client client, String tokenKey, String tokenTypeHint) throws OAuthServiceException { ServerAccessToken accessToken = null; if (!OAuthConstants.REFRESH_TOKEN.equals(tokenTypeHint)) { accessToken = revokeAccessToken(tokenKey); } if (accessToken != null) { handleLinkedRefreshToken(accessToken); } else if (!OAuthConstants.ACCESS_TOKEN.equals(tokenTypeHint)) { RefreshToken currentRefreshToken = revokeRefreshToken(tokenKey); revokeAccessTokens(currentRefreshToken); } } protected void handleLinkedRefreshToken(ServerAccessToken accessToken) { if (accessToken != null && accessToken.getRefreshToken() != null) { RefreshToken rt = getRefreshToken(accessToken.getRefreshToken()); if (rt == null) { return; } unlinkRefreshAccessToken(rt, accessToken.getTokenKey()); if (rt.getAccessTokens().isEmpty()) { revokeRefreshToken(rt.getTokenKey()); } else { saveRefreshToken(rt); } } } protected void revokeAccessTokens(RefreshToken currentRefreshToken) { if (currentRefreshToken != null) { for (String accessTokenKey : currentRefreshToken.getAccessTokens()) { revokeAccessToken(accessTokenKey); } } } protected void unlinkRefreshAccessToken(RefreshToken rt, String tokenKey) { List<String> accessTokenKeys = rt.getAccessTokens(); for (int i = 0; i < accessTokenKeys.size(); i++) { if (accessTokenKeys.get(i).equals(tokenKey)) { accessTokenKeys.remove(i); break; } } } @Override public List<OAuthPermission> convertScopeToPermissions(Client client, List<String> requestedScopes) { checkRequestedScopes(client, requestedScopes); if (requestedScopes.isEmpty()) { return Collections.emptyList(); } else { List<OAuthPermission> list = new ArrayList<>(); for (String scope : requestedScopes) { convertSingleScopeToPermission(client, scope, list); } if (!list.isEmpty()) { return list; } } throw new OAuthServiceException("Requested scopes can not be mapped"); } protected void checkRequestedScopes(Client client, List<String> requestedScopes) { if (requiredScopes != null && !requestedScopes.containsAll(requiredScopes)) { throw new OAuthServiceException("Required scopes are missing"); } } protected void convertSingleScopeToPermission(Client client, String scope, List<OAuthPermission> perms) { OAuthPermission permission = permissionMap.get(scope); if (permission == null) { throw new OAuthServiceException("Unexpected scope: " + scope); } perms.add(permission); } @Override public ServerAccessToken getPreauthorizedToken(Client client, List<String> requestedScopes, UserSubject sub, String grantType) throws OAuthServiceException { if (!isSupportPreauthorizedTokens()) { return null; } ServerAccessToken token = null; for (ServerAccessToken at : getAccessTokens(client, sub)) { if (at.getClient().getClientId().equals(client.getClientId()) && at.getGrantType().equals(grantType) && (sub == null || at.getSubject().getLogin().equals(sub.getLogin()))) { token = at; break; } } if (token != null && OAuthUtils.isExpired(token.getIssuedAt(), token.getExpiresIn())) { revokeToken(client, token.getTokenKey(), OAuthConstants.ACCESS_TOKEN); token = null; } return token; } protected boolean isRefreshTokenSupported(List<String> theScopes) { return theScopes.contains(OAuthConstants.REFRESH_TOKEN_SCOPE); } protected String getCurrentRequestedGrantType() { return messageContext != null ? (String)messageContext.get(OAuthConstants.GRANT_TYPE) : null; } protected String getCurrentClientSecret() { return messageContext != null ? (String)messageContext.get(OAuthConstants.CLIENT_SECRET) : null; } protected MultivaluedMap<String, String> getCurrentTokenRequestParams() { if (messageContext != null) { @SuppressWarnings("unchecked") MultivaluedMap<String, String> params = (MultivaluedMap<String, String>)messageContext.get(OAuthConstants.TOKEN_REQUEST_PARAMS); return params; } else { return null; } } protected RefreshToken updateRefreshToken(RefreshToken rt, ServerAccessToken at) { linkAccessTokenToRefreshToken(rt, at); saveRefreshToken(rt); linkRefreshTokenToAccessToken(rt, at); return rt; } protected RefreshToken createNewRefreshToken(ServerAccessToken at) { RefreshToken rt = doCreateNewRefreshToken(at); return updateRefreshToken(rt, at); } protected RefreshToken doCreateNewRefreshToken(ServerAccessToken at) { RefreshToken rt = new RefreshToken(at.getClient(), refreshTokenLifetime); if (at.getAudiences() != null) { List<String> audiences = new LinkedList<String>(); audiences.addAll(at.getAudiences()); rt.setAudiences(audiences); } rt.setGrantType(at.getGrantType()); if (at.getScopes() != null) { List<OAuthPermission> scopes = new LinkedList<OAuthPermission>(); scopes.addAll(at.getScopes()); rt.setScopes(scopes); } rt.setGrantCode(at.getGrantCode()); rt.setNonce(at.getNonce()); rt.setSubject(at.getSubject()); rt.setClientCodeVerifier(at.getClientCodeVerifier()); return rt; } protected void linkAccessTokenToRefreshToken(RefreshToken rt, ServerAccessToken at) { rt.getAccessTokens().add(at.getTokenKey()); } protected void linkRefreshTokenToAccessToken(RefreshToken rt, ServerAccessToken at) { at.setRefreshToken(rt.getTokenKey()); } protected ServerAccessToken doRefreshAccessToken(Client client, RefreshToken oldRefreshToken, List<String> restrictedScopes) { ServerAccessToken at = createNewAccessToken(client, oldRefreshToken.getSubject()); at.setAudiences(oldRefreshToken.getAudiences() != null ? new ArrayList<String>(oldRefreshToken.getAudiences()) : null); at.setGrantType(oldRefreshToken.getGrantType()); at.setGrantCode(oldRefreshToken.getGrantCode()); at.setSubject(oldRefreshToken.getSubject()); at.setNonce(oldRefreshToken.getNonce()); at.setClientCodeVerifier(oldRefreshToken.getClientCodeVerifier()); if (restrictedScopes.isEmpty()) { at.setScopes(oldRefreshToken.getScopes() != null ? new ArrayList<OAuthPermission>(oldRefreshToken.getScopes()) : null); } else { List<OAuthPermission> theNewScopes = convertScopeToPermissions(client, restrictedScopes); if (oldRefreshToken.getScopes().containsAll(theNewScopes)) { at.setScopes(theNewScopes); } else { throw new OAuthServiceException("Invalid scopes"); } } return at; } public void setAccessTokenLifetime(long accessTokenLifetime) { this.accessTokenLifetime = accessTokenLifetime; } public void setRefreshTokenLifetime(long refreshTokenLifetime) { this.refreshTokenLifetime = refreshTokenLifetime; } public void setRecycleRefreshTokens(boolean recycleRefreshTokens) { this.recycleRefreshTokens = recycleRefreshTokens; } public void init() { for (OAuthPermission perm : permissionMap.values()) { if (defaultScopes != null && defaultScopes.contains(perm.getPermission())) { perm.setDefaultPermission(true); } if (invisibleToClientScopes != null && invisibleToClientScopes.contains(perm.getPermission())) { perm.setInvisibleToClient(true); } } } public void close() { } public Map<String, OAuthPermission> getPermissionMap() { return permissionMap; } public void setPermissionMap(Map<String, OAuthPermission> permissionMap) { this.permissionMap = permissionMap; } public void setSupportedScopes(Map<String, String> scopes) { for (Map.Entry<String, String> entry : scopes.entrySet()) { OAuthPermission permission = new OAuthPermission(entry.getKey(), entry.getValue()); permissionMap.put(entry.getKey(), permission); } } public MessageContext getMessageContext() { return messageContext; } public void setMessageContext(MessageContext messageContext) { this.messageContext = messageContext; if (authenticationStrategy != null) { OAuthUtils.injectContextIntoOAuthProvider(messageContext, authenticationStrategy); } } protected void removeClientTokens(Client c) { List<RefreshToken> refreshTokens = getRefreshTokens(c, null); if (refreshTokens != null) { for (RefreshToken rt : refreshTokens) { revokeRefreshToken(rt.getTokenKey()); } } List<ServerAccessToken> accessTokens = getAccessTokens(c, null); if (accessTokens != null) { for (ServerAccessToken at : accessTokens) { revokeAccessToken(at.getTokenKey()); } } } @Override public Client removeClient(String clientId) { Client c = doGetClient(clientId); removeClientTokens(c); doRemoveClient(c); return c; } @Override public Client getClient(String clientId) { Client client = doGetClient(clientId); if (client != null) { return client; } String grantType = getCurrentRequestedGrantType(); if (OAuthConstants.CLIENT_CREDENTIALS_GRANT.equals(grantType)) { String clientSecret = getCurrentClientSecret(); if (clientSecret != null) { return createClientCredentialsClient(clientId, clientSecret); } } return null; } public void setAuthenticationStrategy(ProviderAuthenticationStrategy authenticationStrategy) { this.authenticationStrategy = authenticationStrategy; } protected boolean authenticateUnregisteredClient(String clientId, String clientSecret) { return authenticationStrategy != null && authenticationStrategy.authenticate(clientId, clientSecret); } protected Client createClientCredentialsClient(String clientId, String password) { if (authenticateUnregisteredClient(clientId, password)) { Client c = new Client(clientId, password, true); c.setAllowedGrantTypes(Collections.singletonList(OAuthConstants.CLIENT_CREDENTIALS_GRANT)); return c; } return null; } protected ServerAccessToken revokeAccessToken(String accessTokenKey) { ServerAccessToken at = getAccessToken(accessTokenKey); if (at != null) { doRevokeAccessToken(at); } return at; } protected RefreshToken revokeRefreshToken(String refreshTokenKey) { RefreshToken refreshToken = getRefreshToken(refreshTokenKey); if (refreshToken != null) { doRevokeRefreshToken(refreshToken); } return refreshToken; } protected abstract void saveAccessToken(ServerAccessToken serverToken); protected abstract void saveRefreshToken(RefreshToken refreshToken); protected abstract void doRevokeAccessToken(ServerAccessToken accessToken); protected abstract void doRevokeRefreshToken(RefreshToken refreshToken); protected abstract RefreshToken getRefreshToken(String refreshTokenKey); protected abstract Client doGetClient(String clientId); protected abstract void doRemoveClient(Client c); public List<String> getDefaultScopes() { return defaultScopes; } public void setDefaultScopes(List<String> defaultScopes) { this.defaultScopes = defaultScopes; } public List<String> getRequiredScopes() { return requiredScopes; } public void setRequiredScopes(List<String> requiredScopes) { this.requiredScopes = requiredScopes; } public List<String> getInvisibleToClientScopes() { return invisibleToClientScopes; } public void setInvisibleToClientScopes(List<String> invisibleToClientScopes) { this.invisibleToClientScopes = invisibleToClientScopes; } public boolean isSupportPreauthorizedTokens() { return supportPreauthorizedTokens; } public void setSupportPreauthorizedTokens(boolean supportPreauthorizedTokens) { this.supportPreauthorizedTokens = supportPreauthorizedTokens; } protected static boolean isClientMatched(Client c, UserSubject resourceOwner) { return resourceOwner == null || c.getResourceOwnerSubject() != null && c.getResourceOwnerSubject().getLogin().equals(resourceOwner.getLogin()); } protected static boolean isTokenMatched(ServerAccessToken token, Client c, UserSubject sub) { if (token != null && (c == null || token.getClient().getClientId().equals(c.getClientId()))) { UserSubject tokenSub = token.getSubject(); if (sub == null || tokenSub != null && tokenSub.getLogin().equals(sub.getLogin())) { return true; } } return false; } public void setClients(List<Client> clients) { for (Client c : clients) { setClient(c); } } public boolean isUseJwtFormatForAccessTokens() { return useJwtFormatForAccessTokens; } public void setUseJwtFormatForAccessTokens(boolean useJwtFormatForAccessTokens) { this.useJwtFormatForAccessTokens = useJwtFormatForAccessTokens; } public OAuthJoseJwtProducer getJwtAccessTokenProducer() { return jwtAccessTokenProducer; } public void setJwtAccessTokenProducer(OAuthJoseJwtProducer jwtAccessTokenProducer) { this.jwtAccessTokenProducer = jwtAccessTokenProducer; } protected String processJwtAccessToken(JwtClaims jwtCliams) { // It will JWS-sign (default) and/or JWE-encrypt OAuthJoseJwtProducer processor = getJwtAccessTokenProducer() == null ? new OAuthJoseJwtProducer() : getJwtAccessTokenProducer(); return processor.processJwt(new JwtToken(jwtCliams)); } public Map<String, String> getJwtAccessTokenClaimMap() { return jwtAccessTokenClaimMap; } public void setJwtAccessTokenClaimMap(Map<String, String> jwtAccessTokenClaimMap) { this.jwtAccessTokenClaimMap = jwtAccessTokenClaimMap; } }