/**
* 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.filters;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import javax.annotation.Priority;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.security.SimplePrincipal;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.jaxrs.provider.FormEncodingProvider;
import org.apache.cxf.jaxrs.utils.ExceptionUtils;
import org.apache.cxf.jaxrs.utils.FormUtils;
import org.apache.cxf.jaxrs.utils.JAXRSUtils;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.rs.security.jose.common.JoseConstants;
import org.apache.cxf.rs.security.oauth2.common.AccessTokenValidation;
import org.apache.cxf.rs.security.oauth2.common.AuthenticationMethod;
import org.apache.cxf.rs.security.oauth2.common.OAuthContext;
import org.apache.cxf.rs.security.oauth2.common.OAuthPermission;
import org.apache.cxf.rs.security.oauth2.common.UserSubject;
import org.apache.cxf.rs.security.oauth2.services.AbstractAccessTokenValidator;
import org.apache.cxf.rs.security.oauth2.utils.AuthorizationUtils;
import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
import org.apache.cxf.rs.security.oauth2.utils.OAuthUtils;
import org.apache.cxf.security.SecurityContext;
import org.apache.cxf.security.transport.TLSSessionInfo;
/**
* JAX-RS OAuth2 filter which can be used to protect the end-user endpoints
*/
@Provider
@PreMatching
// Priorities.AUTHORIZATION also works
@Priority(Priorities.AUTHENTICATION)
public class OAuthRequestFilter extends AbstractAccessTokenValidator
implements ContainerRequestFilter {
private static final Logger LOG = LogUtils.getL7dLogger(OAuthRequestFilter.class);
private boolean useUserSubject;
private String audience;
private String issuer;
private boolean completeAudienceMatch;
private boolean audienceIsEndpointAddress = true;
private boolean checkFormData;
private List<String> requiredScopes = Collections.emptyList();
private boolean allPermissionsMatch;
private boolean blockPublicClients;
private AuthenticationMethod am;
@Override
public void filter(ContainerRequestContext context) {
validateRequest(JAXRSUtils.getCurrentMessage());
}
protected void validateRequest(Message m) {
if (isCorsRequest(m)) {
return;
}
// Get the scheme and its data, Bearer only is supported by default
// WWW-Authenticate with the list of supported schemes will be sent back
// if the scheme is not accepted
String[] authParts = getAuthorizationParts(m);
if (authParts.length < 2) {
throw ExceptionUtils.toForbiddenException(null, null);
}
String authScheme = authParts[0];
String authSchemeData = authParts[1];
// Get the access token
AccessTokenValidation accessTokenV = getAccessTokenValidation(authScheme, authSchemeData, null);
if (!accessTokenV.isInitialValidationSuccessful()) {
AuthorizationUtils.throwAuthorizationFailure(supportedSchemes, realm);
}
// Check audiences
String validAudience = validateAudiences(accessTokenV.getAudiences());
// Check if token was issued by the supported issuer
if (issuer != null && !issuer.equals(accessTokenV.getTokenIssuer())) {
AuthorizationUtils.throwAuthorizationFailure(supportedSchemes, realm);
}
// Find the scopes which match the current request
List<OAuthPermission> permissions = accessTokenV.getTokenScopes();
List<OAuthPermission> matchingPermissions = new ArrayList<>();
HttpServletRequest req = getMessageContext().getHttpServletRequest();
for (OAuthPermission perm : permissions) {
boolean uriOK = checkRequestURI(req, perm.getUris());
boolean verbOK = checkHttpVerb(req, perm.getHttpVerbs());
boolean scopeOk = checkScopeProperty(perm.getPermission());
if (uriOK && verbOK && scopeOk) {
matchingPermissions.add(perm);
}
}
if (!permissions.isEmpty() && matchingPermissions.isEmpty()
|| allPermissionsMatch && (matchingPermissions.size() != permissions.size())
|| !requiredScopes.isEmpty() && requiredScopes.size() != matchingPermissions.size()) {
String message = "Client has no valid permissions";
LOG.warning(message);
throw ExceptionUtils.toForbiddenException(null, null);
}
if (accessTokenV.getClientIpAddress() != null) {
String remoteAddress = getMessageContext().getHttpServletRequest().getRemoteAddr();
if (remoteAddress == null || accessTokenV.getClientIpAddress().equals(remoteAddress)) {
String message = "Client IP Address is invalid";
LOG.warning(message);
throw ExceptionUtils.toForbiddenException(null, null);
}
}
if (blockPublicClients && !accessTokenV.isClientConfidential()) {
String message = "Only Confidential Clients are supported";
LOG.warning(message);
throw ExceptionUtils.toForbiddenException(null, null);
}
if (am != null && !am.equals(accessTokenV.getTokenSubject().getAuthenticationMethod())) {
String message = "The token has been authorized by the resource owner "
+ "using an unsupported authentication method";
LOG.warning(message);
throw ExceptionUtils.toNotAuthorizedException(null, null);
}
// Check Client Certificate Binding if any
String certThumbprint = accessTokenV.getExtraProps().get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256);
if (certThumbprint != null) {
TLSSessionInfo tlsInfo = getTlsSessionInfo();
X509Certificate cert = tlsInfo == null ? null : OAuthUtils.getRootTLSCertificate(tlsInfo);
if (cert == null || !OAuthUtils.compareCertificateThumbprints(cert, certThumbprint)) {
throw ExceptionUtils.toNotAuthorizedException(null, null);
}
}
// Create the security context and make it available on the message
SecurityContext sc = createSecurityContext(req, accessTokenV);
m.put(SecurityContext.class, sc);
// Also set the OAuthContext
OAuthContext oauthContext = new OAuthContext(accessTokenV.getTokenSubject(),
accessTokenV.getClientSubject(),
matchingPermissions,
accessTokenV.getTokenGrantType());
oauthContext.setClientId(accessTokenV.getClientId());
oauthContext.setClientConfidential(accessTokenV.isClientConfidential());
oauthContext.setTokenKey(accessTokenV.getTokenKey());
oauthContext.setTokenAudience(validAudience);
oauthContext.setTokenIssuer(accessTokenV.getTokenIssuer());
oauthContext.setTokenRequestParts(authParts);
oauthContext.setTokenExtraProperties(accessTokenV.getExtraProps());
m.setContent(OAuthContext.class, oauthContext);
}
protected boolean checkHttpVerb(HttpServletRequest req, List<String> verbs) {
if (!verbs.isEmpty()
&& !verbs.contains(req.getMethod())) {
String message = "Invalid http verb";
LOG.fine(message);
return false;
}
return true;
}
protected boolean checkRequestURI(HttpServletRequest request, List<String> uris) {
if (uris.isEmpty()) {
return true;
}
String servletPath = request.getPathInfo();
boolean foundValidScope = false;
for (String uri : uris) {
if (OAuthUtils.checkRequestURI(servletPath, uri)) {
foundValidScope = true;
break;
}
}
if (!foundValidScope) {
String message = "Invalid request URI: " + request.getRequestURL().toString();
LOG.fine(message);
}
return foundValidScope;
}
protected boolean checkScopeProperty(String scope) {
if (!requiredScopes.isEmpty()) {
return requiredScopes.contains(scope);
} else {
return true;
}
}
public void setUseUserSubject(boolean useUserSubject) {
this.useUserSubject = useUserSubject;
}
protected SecurityContext createSecurityContext(HttpServletRequest request,
AccessTokenValidation accessTokenV) {
UserSubject resourceOwnerSubject = accessTokenV.getTokenSubject();
UserSubject clientSubject = accessTokenV.getClientSubject();
final UserSubject theSubject =
OAuthRequestFilter.this.useUserSubject ? resourceOwnerSubject : clientSubject;
return new SecurityContext() {
public Principal getUserPrincipal() {
return theSubject != null ? new SimplePrincipal(theSubject.getLogin()) : null;
}
public boolean isUserInRole(String role) {
if (theSubject == null) {
return false;
}
return theSubject.getRoles().contains(role);
}
};
}
protected boolean isCorsRequest(Message m) {
//Redirection-based flows (Implicit Grant Flow specifically) may have
//the browser issuing CORS preflight OPTIONS request.
//org.apache.cxf.rs.security.cors.CrossOriginResourceSharingFilter can be
//used to handle preflights but local preflights (to be handled by the service code)
// will be blocked by this filter unless CORS filter has done the initial validation
// and set a message "local_preflight" property to true
return MessageUtils.isTrue(m.get("local_preflight"));
}
protected String validateAudiences(List<String> audiences) {
if (StringUtils.isEmpty(audiences) && audience == null) {
return null;
}
if (audience != null) {
if (audiences.contains(audience)) {
return audience;
}
AuthorizationUtils.throwAuthorizationFailure(supportedSchemes, realm);
}
if (!audienceIsEndpointAddress) {
return null;
}
String requestPath = (String)PhaseInterceptorChain.getCurrentMessage().get(Message.REQUEST_URL);
for (String s : audiences) {
boolean matched = completeAudienceMatch ? requestPath.equals(s) : requestPath.startsWith(s);
if (matched) {
return s;
}
}
AuthorizationUtils.throwAuthorizationFailure(supportedSchemes, realm);
return null;
}
public void setCheckFormData(boolean checkFormData) {
this.checkFormData = checkFormData;
}
protected String[] getAuthorizationParts(Message m) {
if (!checkFormData) {
return AuthorizationUtils.getAuthorizationParts(getMessageContext(), supportedSchemes);
} else {
return new String[]{OAuthConstants.BEARER_AUTHORIZATION_SCHEME, getTokenFromFormData(m)};
}
}
protected String getTokenFromFormData(Message message) {
String method = (String)message.get(Message.HTTP_REQUEST_METHOD);
String type = (String)message.get(Message.CONTENT_TYPE);
if (type != null && MediaType.APPLICATION_FORM_URLENCODED.startsWith(type)
&& method != null && (method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT))) {
try {
FormEncodingProvider<Form> provider = new FormEncodingProvider<Form>(true);
Form form = FormUtils.readForm(provider, message);
MultivaluedMap<String, String> formData = form.asMap();
String token = formData.getFirst(OAuthConstants.ACCESS_TOKEN);
if (token != null) {
FormUtils.restoreForm(provider, form, message);
return token;
}
} catch (Exception ex) {
// the exception will be thrown below
}
}
AuthorizationUtils.throwAuthorizationFailure(supportedSchemes, realm);
return null;
}
public void setRequiredScopes(List<String> requiredScopes) {
this.requiredScopes = requiredScopes;
}
public void setAllPermissionsMatch(boolean allPermissionsMatch) {
this.allPermissionsMatch = allPermissionsMatch;
}
public void setBlockPublicClients(boolean blockPublicClients) {
this.blockPublicClients = blockPublicClients;
}
public void setTokenSubjectAuthenticationMethod(AuthenticationMethod method) {
this.am = method;
}
public String getAudience() {
return audience;
}
public void setAudience(String audience) {
this.audience = audience;
}
public boolean isCompleteAudienceMatch() {
return completeAudienceMatch;
}
public void setCompleteAudienceMatch(boolean completeAudienceMatch) {
this.completeAudienceMatch = completeAudienceMatch;
}
public void setAudienceIsEndpointAddress(boolean audienceIsEndpointAddress) {
this.audienceIsEndpointAddress = audienceIsEndpointAddress;
}
public void setIssuer(String issuer) {
this.issuer = issuer;
}
private TLSSessionInfo getTlsSessionInfo() {
return (TLSSessionInfo)getMessageContext().get(TLSSessionInfo.class.getName());
}
}