/**
* 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.services;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.jaxrs.utils.ExceptionUtils;
import org.apache.cxf.rs.security.oauth2.common.Client;
import org.apache.cxf.rs.security.oauth2.common.OAuthAuthorizationData;
import org.apache.cxf.rs.security.oauth2.common.OAuthPermission;
import org.apache.cxf.rs.security.oauth2.common.OAuthRedirectionState;
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.provider.AuthorizationRequestFilter;
import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
import org.apache.cxf.rs.security.oauth2.provider.ResourceOwnerNameProvider;
import org.apache.cxf.rs.security.oauth2.provider.SessionAuthenticityTokenProvider;
import org.apache.cxf.rs.security.oauth2.provider.SubjectCreator;
import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
import org.apache.cxf.rs.security.oauth2.utils.OAuthUtils;
import org.apache.cxf.security.SecurityContext;
/**
* The Base Redirection-Based Grant Service
*/
public abstract class RedirectionBasedGrantService extends AbstractOAuthService {
private static final String AUTHORIZATION_REQUEST_PARAMETERS = "authorization.request.parameters";
private Set<String> supportedResponseTypes;
private String supportedGrantType;
private boolean useAllClientScopes;
private boolean partialMatchScopeValidation;
private boolean useRegisteredRedirectUriIfPossible = true;
private SessionAuthenticityTokenProvider sessionAuthenticityTokenProvider;
private SubjectCreator subjectCreator;
private ResourceOwnerNameProvider resourceOwnerNameProvider;
private int maxDefaultSessionInterval;
private boolean matchRedirectUriWithApplicationUri;
private boolean hidePreauthorizedScopesInForm;
private AuthorizationRequestFilter authorizationFilter;
private List<String> scopesRequiringNoConsent;
private boolean supportSinglePageApplications = true;
protected RedirectionBasedGrantService(String supportedResponseType,
String supportedGrantType) {
this(Collections.singleton(supportedResponseType), supportedGrantType);
}
protected RedirectionBasedGrantService(Set<String> supportedResponseTypes,
String supportedGrantType) {
this.supportedResponseTypes = supportedResponseTypes;
this.supportedGrantType = supportedGrantType;
}
/**
* Handles the initial authorization request by preparing
* the authorization challenge data and returning it to the user.
* Typically the data are expected to be presented in the HTML form
* @return the authorization data
*/
@GET
@Produces({"application/xhtml+xml", "text/html", "application/xml", "application/json" })
public Response authorize() {
MultivaluedMap<String, String> params = getQueryParameters();
return startAuthorization(params);
}
/**
* Processes the end user decision
* @return The grant value, authorization code or the token
*/
@GET
@Path("/decision")
public Response authorizeDecision() {
MultivaluedMap<String, String> params = getQueryParameters();
return completeAuthorization(params);
}
/**
* Processes the end user decision
* @return The grant value, authorization code or the token
*/
@POST
@Path("/decision")
@Consumes("application/x-www-form-urlencoded")
public Response authorizeDecisionForm(MultivaluedMap<String, String> params) {
return completeAuthorization(params);
}
/**
* Starts the authorization process
*/
protected Response startAuthorization(MultivaluedMap<String, String> params) {
// Make sure the end user has authenticated, check if HTTPS is used
SecurityContext sc = getAndValidateSecurityContext(params);
Client client = getClient(params.getFirst(OAuthConstants.CLIENT_ID), params);
// Create a UserSubject representing the end user
UserSubject userSubject = createUserSubject(sc, params);
if (authorizationFilter != null) {
params = authorizationFilter.process(params, userSubject, client);
}
// Validate the provided request URI, if any, against the ones Client provided
// during the registration
String redirectUri = validateRedirectUri(client, params.getFirst(OAuthConstants.REDIRECT_URI));
return startAuthorization(params, userSubject, client, redirectUri);
}
protected Response startAuthorization(MultivaluedMap<String, String> params,
UserSubject userSubject,
Client client,
String redirectUri) {
// Enforce the client confidentiality requirements
if (!OAuthUtils.isGrantSupportedForClient(client, canSupportPublicClient(client), supportedGrantType)) {
LOG.fine("The grant type is not supported");
return createErrorResponse(params, redirectUri, OAuthConstants.UNAUTHORIZED_CLIENT);
}
// Check response_type
String responseType = params.getFirst(OAuthConstants.RESPONSE_TYPE);
if (responseType == null || !getSupportedResponseTypes().contains(responseType)) {
LOG.fine("The response type is null or not supported");
return createErrorResponse(params, redirectUri, OAuthConstants.UNSUPPORTED_RESPONSE_TYPE);
}
// Get the requested scopes
String providedScope = params.getFirst(OAuthConstants.SCOPE);
List<String> requestedScope = null;
List<OAuthPermission> requestedPermissions = null;
try {
requestedScope = OAuthUtils.getRequestedScopes(client,
providedScope,
useAllClientScopes,
partialMatchScopeValidation);
requestedPermissions = getDataProvider().convertScopeToPermissions(client, requestedScope);
} catch (OAuthServiceException ex) {
LOG.log(Level.FINE, "Error processing scopes", ex);
return createErrorResponse(params, redirectUri, OAuthConstants.INVALID_SCOPE);
}
// Validate the audience
String clientAudience = params.getFirst(OAuthConstants.CLIENT_AUDIENCE);
// Right now if the audience parameter is set it is expected to be contained
// in the list of Client audiences set at the Client registration time.
if (!OAuthUtils.validateAudience(clientAudience, client.getRegisteredAudiences())) {
LOG.fine("Error validating audience parameter");
return createErrorResponse(params, redirectUri, OAuthConstants.INVALID_REQUEST);
}
// Request a new grant only if no pre-authorized token is available
ServerAccessToken preAuthorizedToken = null;
if (canAccessTokenBeReturned(responseType)) {
preAuthorizedToken = getDataProvider().getPreauthorizedToken(client, requestedScope, userSubject,
supportedGrantType);
}
List<OAuthPermission> alreadyAuthorizedPerms = null;
boolean preAuthorizationComplete = false;
if (preAuthorizedToken != null) {
alreadyAuthorizedPerms = preAuthorizedToken.getScopes();
preAuthorizationComplete =
OAuthUtils.convertPermissionsToScopeList(alreadyAuthorizedPerms).containsAll(requestedScope);
if (!preAuthorizationComplete) {
preAuthorizedToken = null;
}
}
Response finalResponse = null;
try {
final boolean authorizationCanBeSkipped = preAuthorizationComplete
|| canAuthorizationBeSkipped(params, client, userSubject, requestedScope, requestedPermissions);
// Populate the authorization challenge data
OAuthAuthorizationData data = createAuthorizationData(client, params, redirectUri, userSubject,
requestedPermissions,
alreadyAuthorizedPerms,
authorizationCanBeSkipped);
if (authorizationCanBeSkipped) {
getMessageContext().put(AUTHORIZATION_REQUEST_PARAMETERS, params);
List<OAuthPermission> approvedScopes =
preAuthorizationComplete ? preAuthorizedToken.getScopes() : requestedPermissions;
finalResponse = createGrant(data,
client,
requestedScope,
OAuthUtils.convertPermissionsToScopeList(approvedScopes),
userSubject,
preAuthorizedToken);
} else {
finalResponse = Response.ok(data).build();
}
} catch (OAuthServiceException ex) {
finalResponse = createErrorResponse(params, redirectUri, ex.getError().getError());
}
return finalResponse;
}
//CHECKSTYLE:OFF
public Set<String> getSupportedResponseTypes() {
return supportedResponseTypes;
}
protected boolean canAuthorizationBeSkipped(MultivaluedMap<String, String> params,
Client client,
UserSubject userSubject,
List<String> requestedScope,
List<OAuthPermission> permissions) {
return noConsentForRequestedScopes(params, client, userSubject, requestedScope, permissions);
}
protected boolean noConsentForRequestedScopes(MultivaluedMap<String, String> params,
Client client,
UserSubject userSubject,
List<String> requestedScope,
List<OAuthPermission> permissions) {
return scopesRequiringNoConsent != null
&& requestedScope != null
&& scopesRequiringNoConsent.containsAll(requestedScope);
}
/**
* Create the authorization challenge data
*/
protected OAuthAuthorizationData createAuthorizationData(Client client,
MultivaluedMap<String, String> params,
String redirectUri,
UserSubject subject,
List<OAuthPermission> requestedPerms,
List<OAuthPermission> alreadyAuthorizedPerms,
boolean authorizationCanBeSkipped) {
OAuthAuthorizationData secData = new OAuthAuthorizationData();
secData.setState(params.getFirst(OAuthConstants.STATE));
secData.setRedirectUri(redirectUri);
secData.setAudience(params.getFirst(OAuthConstants.CLIENT_AUDIENCE));
secData.setNonce(params.getFirst(OAuthConstants.NONCE));
secData.setClientId(client.getClientId());
secData.setResponseType(params.getFirst(OAuthConstants.RESPONSE_TYPE));
if (requestedPerms != null && !requestedPerms.isEmpty()) {
StringBuilder builder = new StringBuilder();
for (OAuthPermission perm : requestedPerms) {
builder.append(perm.getPermission() + " ");
}
secData.setProposedScope(builder.toString().trim());
}
if (!authorizationCanBeSkipped) {
secData.setPermissions(requestedPerms);
secData.setAlreadyAuthorizedPermissions(alreadyAuthorizedPerms);
secData.setHidePreauthorizedScopesInForm(hidePreauthorizedScopesInForm);
secData.setApplicationName(client.getApplicationName());
secData.setApplicationWebUri(client.getApplicationWebUri());
secData.setApplicationDescription(client.getApplicationDescription());
secData.setApplicationLogoUri(client.getApplicationLogoUri());
secData.setApplicationCertificates(client.getApplicationCertificates());
Map<String, String> extraProperties = client.getProperties();
secData.setExtraApplicationProperties(extraProperties);
secData.setApplicationRegisteredDynamically(client.isRegisteredDynamically());
secData.setSupportSinglePageApplications(supportSinglePageApplications);
String replyTo = getMessageContext().getUriInfo()
.getAbsolutePathBuilder().path("decision").build().toString();
secData.setReplyTo(replyTo);
personalizeData(secData, subject);
addAuthenticityTokenToSession(secData, params, subject);
}
return secData;
}
protected OAuthRedirectionState recreateRedirectionStateFromSession(
UserSubject subject, String sessionToken) {
if (sessionAuthenticityTokenProvider != null) {
return sessionAuthenticityTokenProvider.getSessionState(super.getMessageContext(),
sessionToken,
subject);
} else {
return null;
}
}
protected OAuthRedirectionState recreateRedirectionStateFromParams(MultivaluedMap<String, String> params) {
OAuthRedirectionState state = new OAuthRedirectionState();
state.setClientId(params.getFirst(OAuthConstants.CLIENT_ID));
state.setRedirectUri(params.getFirst(OAuthConstants.REDIRECT_URI));
state.setAudience(params.getFirst(OAuthConstants.CLIENT_AUDIENCE));
state.setProposedScope(params.getFirst(OAuthConstants.SCOPE));
state.setState(params.getFirst(OAuthConstants.STATE));
state.setNonce(params.getFirst(OAuthConstants.NONCE));
state.setResponseType(params.getFirst(OAuthConstants.RESPONSE_TYPE));
return state;
}
protected void personalizeData(OAuthAuthorizationData data, UserSubject userSubject) {
if (resourceOwnerNameProvider != null) {
data.setEndUserName(resourceOwnerNameProvider.getName(userSubject));
}
}
protected List<String> getApprovedScope(List<String> requestedScope, List<String> approvedScope) {
if (StringUtils.isEmpty(approvedScope)) {
// no down-scoping done by a user, all of the requested scopes have been authorized
return requestedScope;
} else {
return approvedScope;
}
}
/**
* Completes the authorization process
*/
protected Response completeAuthorization(MultivaluedMap<String, String> params) {
// Make sure the end user has authenticated, check if HTTPS is used
SecurityContext securityContext = getAndValidateSecurityContext(params);
UserSubject userSubject = createUserSubject(securityContext, params);
// Make sure the session is valid
String sessionTokenParamName = params.getFirst(OAuthConstants.SESSION_AUTHENTICITY_TOKEN_PARAM_NAME);
if (sessionTokenParamName == null) {
sessionTokenParamName = OAuthConstants.SESSION_AUTHENTICITY_TOKEN;
}
String sessionToken = params.getFirst(sessionTokenParamName);
if (sessionToken == null || !compareRequestAndSessionTokens(sessionToken, params, userSubject)) {
throw ExceptionUtils.toBadRequestException(null, null);
}
OAuthRedirectionState state = recreateRedirectionStateFromSession(userSubject, sessionToken);
if (state == null) {
state = recreateRedirectionStateFromParams(params);
}
Client client = getClient(state.getClientId(), params);
String redirectUri = validateRedirectUri(client, state.getRedirectUri());
// Get the end user decision value
String decision = params.getFirst(OAuthConstants.AUTHORIZATION_DECISION_KEY);
boolean allow = OAuthConstants.AUTHORIZATION_DECISION_ALLOW.equals(decision);
// Return the error if denied
if (!allow) {
return createErrorResponse(params, redirectUri, OAuthConstants.ACCESS_DENIED);
}
// Check if the end user may have had a chance to down-scope the requested scopes
List<String> requestedScope = OAuthUtils.parseScope(state.getProposedScope());
List<String> approvedScope = new LinkedList<String>();
for (String rScope : requestedScope) {
String param = params.getFirst(rScope + "_status");
if (param != null && OAuthConstants.AUTHORIZATION_DECISION_ALLOW.equals(param)) {
approvedScope.add(rScope);
}
}
if (!requestedScope.containsAll(approvedScope)
|| !OAuthUtils.validateScopes(requestedScope, client.getRegisteredScopes(),
partialMatchScopeValidation)) {
return createErrorResponse(params, redirectUri, OAuthConstants.INVALID_SCOPE);
}
getMessageContext().put(AUTHORIZATION_REQUEST_PARAMETERS, params);
// Request a new grant
return createGrant(state,
client,
requestedScope,
approvedScope,
userSubject,
null);
}
public void setSessionAuthenticityTokenProvider(SessionAuthenticityTokenProvider sessionAuthenticityTokenProvider) {
this.sessionAuthenticityTokenProvider = sessionAuthenticityTokenProvider;
}
public void setSubjectCreator(SubjectCreator creator) {
this.subjectCreator = creator;
}
protected UserSubject createUserSubject(SecurityContext securityContext,
MultivaluedMap<String, String> params) {
UserSubject subject = null;
if (subjectCreator != null) {
subject = subjectCreator.createUserSubject(getMessageContext(),
params);
if (subject != null) {
return subject;
}
}
return OAuthUtils.createSubject(getMessageContext(), securityContext);
}
protected Response createErrorResponse(MultivaluedMap<String, String> params,
String redirectUri,
String error) {
return createErrorResponse(params.getFirst(OAuthConstants.STATE), redirectUri, error);
}
protected boolean canAccessTokenBeReturned(String responseType) {
return true;
}
protected abstract Response createErrorResponse(String state,
String redirectUri,
String error);
protected abstract Response createGrant(OAuthRedirectionState state,
Client client,
List<String> requestedScope,
List<String> approvedScope,
UserSubject userSubject,
ServerAccessToken preAuthorizedToken);
protected SecurityContext getAndValidateSecurityContext(MultivaluedMap<String, String> params) {
SecurityContext securityContext =
(SecurityContext)getMessageContext().get(SecurityContext.class.getName());
if (securityContext == null || securityContext.getUserPrincipal() == null) {
throw ExceptionUtils.toNotAuthorizedException(null, null);
}
checkTransportSecurity();
return securityContext;
}
protected String validateRedirectUri(Client client, String redirectUri) {
List<String> uris = client.getRedirectUris();
if (redirectUri != null) {
if (!uris.contains(redirectUri)) {
reportInvalidRequestError("Client Redirect Uri is invalid");
}
} else if (uris.size() == 1 && useRegisteredRedirectUriIfPossible) {
redirectUri = uris.get(0);
}
if (redirectUri == null && uris.size() == 0 && !canRedirectUriBeEmpty(client)) {
reportInvalidRequestError("Client Redirect Uri is invalid");
}
if (redirectUri != null && matchRedirectUriWithApplicationUri
&& client.getApplicationWebUri() != null
&& !redirectUri.startsWith(client.getApplicationWebUri())) {
reportInvalidRequestError("Client Redirect Uri is invalid");
}
return redirectUri;
}
private void addAuthenticityTokenToSession(OAuthAuthorizationData secData,
MultivaluedMap<String, String> params,
UserSubject subject) {
final String sessionToken;
if (this.sessionAuthenticityTokenProvider != null) {
sessionToken = sessionAuthenticityTokenProvider.createSessionToken(getMessageContext(),
params,
subject,
secData);
} else {
sessionToken = OAuthUtils.setSessionToken(getMessageContext(), maxDefaultSessionInterval);
}
secData.setAuthenticityToken(sessionToken);
}
private boolean compareRequestAndSessionTokens(String requestToken,
MultivaluedMap<String, String> params,
UserSubject subject) {
final String sessionToken;
if (this.sessionAuthenticityTokenProvider != null) {
sessionToken = sessionAuthenticityTokenProvider.removeSessionToken(getMessageContext(),
params,
subject);
} else {
sessionToken = OAuthUtils.getSessionToken(getMessageContext());
}
if (StringUtils.isEmpty(sessionToken)) {
return false;
} else {
return requestToken.equals(sessionToken);
}
}
/**
* Get the {@link Client} reference
* @param params request parameters
* @return Client the client reference
* @throws {@link javax.ws.rs.WebApplicationException} if no matching Client is found,
* the error is returned directly to the end user without
* following the redirect URI if any
*/
protected Client getClient(String clientId, MultivaluedMap<String, String> params) {
Client client = null;
try {
client = getValidClient(clientId, params);
} catch (OAuthServiceException ex) {
if (ex.getError() != null) {
reportInvalidRequestError(ex.getError(), null);
}
}
if (client == null) {
reportInvalidRequestError("Client ID is invalid", null);
}
return client;
}
protected Response createHtmlResponse(Object response) {
return Response.ok(response).type(MediaType.TEXT_HTML).build();
}
protected boolean isFormResponse(OAuthRedirectionState state) {
return OAuthConstants.FORM_RESPONSE_MODE.equals(
state.getExtraProperties().get(OAuthConstants.RESPONSE_MODE));
}
protected String getSupportedGrantType() {
return this.supportedGrantType;
}
public void setResourceOwnerNameProvider(ResourceOwnerNameProvider resourceOwnerNameProvider) {
this.resourceOwnerNameProvider = resourceOwnerNameProvider;
}
public void setPartialMatchScopeValidation(boolean partialMatchScopeValidation) {
this.partialMatchScopeValidation = partialMatchScopeValidation;
}
public void setUseAllClientScopes(boolean useAllClientScopes) {
this.useAllClientScopes = useAllClientScopes;
}
/**
* If a client does not include a redirect_uri parameter but has an exactly one
* pre-registered redirect_uri then use that redirect_uri
* @param use allows to use a single registered redirect_uri if set to true (default)
*/
public void setUseRegisteredRedirectUriIfPossible(boolean use) {
this.useRegisteredRedirectUriIfPossible = use;
}
protected abstract boolean canSupportPublicClient(Client c);
protected abstract boolean canRedirectUriBeEmpty(Client c);
public void setMaxDefaultSessionInterval(int maxDefaultSessionInterval) {
this.maxDefaultSessionInterval = maxDefaultSessionInterval;
}
public void setMatchRedirectUriWithApplicationUri(boolean matchRedirectUriWithApplicationUri) {
this.matchRedirectUriWithApplicationUri = matchRedirectUriWithApplicationUri;
}
public void setHidePreauthorizedScopesInForm(boolean hidePreauthorizedScopesInForm) {
this.hidePreauthorizedScopesInForm = hidePreauthorizedScopesInForm;
}
public void setAuthorizationFilter(AuthorizationRequestFilter authorizationFilter) {
this.authorizationFilter = authorizationFilter;
}
public void setScopesRequiringNoConsent(List<String> scopesRequiringNoConsent) {
this.scopesRequiringNoConsent = scopesRequiringNoConsent;
}
public void setSupportSinglePageApplications(boolean supportSinglePageApplications) {
this.supportSinglePageApplications = supportSinglePageApplications;
}
}