blob: 742163166a023e1ab8e0b638251ad4ed7e3c9291 [file] [log] [blame]
/*
* 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.meecrowave.oauth2.configuration;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.cxf.Bus;
import org.apache.cxf.interceptor.security.AuthenticationException;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.jaxrs.utils.JAXRSUtils;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
import org.apache.cxf.rs.security.oauth2.common.AuthenticationMethod;
import org.apache.cxf.rs.security.oauth2.common.Client;
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.grants.AbstractGrantHandler;
import org.apache.cxf.rs.security.oauth2.grants.clientcred.ClientCredentialsGrantHandler;
import org.apache.cxf.rs.security.oauth2.grants.code.AuthorizationCodeGrantHandler;
import org.apache.cxf.rs.security.oauth2.grants.code.DefaultEncryptingCodeDataProvider;
import org.apache.cxf.rs.security.oauth2.grants.code.DigestCodeVerifier;
import org.apache.cxf.rs.security.oauth2.grants.code.JPACodeDataProvider;
import org.apache.cxf.rs.security.oauth2.grants.code.PlainCodeVerifier;
import org.apache.cxf.rs.security.oauth2.grants.jwt.JwtBearerGrantHandler;
import org.apache.cxf.rs.security.oauth2.grants.owner.JAASResourceOwnerLoginHandler;
import org.apache.cxf.rs.security.oauth2.grants.owner.ResourceOwnerGrantHandler;
import org.apache.cxf.rs.security.oauth2.grants.owner.ResourceOwnerLoginHandler;
import org.apache.cxf.rs.security.oauth2.grants.refresh.RefreshTokenGrantHandler;
import org.apache.cxf.rs.security.oauth2.provider.AbstractOAuthDataProvider;
import org.apache.cxf.rs.security.oauth2.provider.AccessTokenGrantHandler;
import org.apache.cxf.rs.security.oauth2.provider.DefaultEncryptingOAuthDataProvider;
import org.apache.cxf.rs.security.oauth2.provider.JCacheOAuthDataProvider;
import org.apache.cxf.rs.security.oauth2.provider.JPAOAuthDataProvider;
import org.apache.cxf.rs.security.oauth2.provider.JoseSessionTokenProvider;
import org.apache.cxf.rs.security.oauth2.provider.OAuthDataProvider;
import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
import org.apache.cxf.rs.security.oauth2.services.AbstractTokenService;
import org.apache.cxf.rs.security.oauth2.services.AccessTokenService;
import org.apache.cxf.rs.security.oauth2.services.AuthorizationCodeGrantService;
import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
import org.apache.meecrowave.Meecrowave;
import org.apache.meecrowave.oauth2.data.RefreshTokenEnabledProvider;
import org.apache.meecrowave.oauth2.provider.JCacheCodeDataProvider;
import javax.annotation.PostConstruct;
import javax.crypto.spec.SecretKeySpec;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MultivaluedMap;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Consumer;
import java.util.function.Function;
import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
import static java.util.Locale.ENGLISH;
import static java.util.Optional.ofNullable;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
import static org.apache.cxf.rs.security.oauth2.common.AuthenticationMethod.PASSWORD;
@ApplicationScoped
public class OAuth2Configurer {
@Inject
private Meecrowave.Builder builder;
@Inject
private Bus bus;
@Inject
private HttpServletRequest request;
@Inject
private JCacheConfigurer jCacheConfigurer;
private OAuth2Options configuration;
private Consumer<AccessTokenService> tokenServiceConsumer;
private Consumer<AuthorizationCodeGrantService> redirectionBasedGrantServiceConsumer;
private Consumer<AbstractTokenService> abstractTokenServiceConsumer;
private Map<String, String> securityProperties;
@PostConstruct // TODO: still some missing configuration for jwt etc to add/wire from OAuth2Options
private void preCompute() {
configuration = builder.getExtension(OAuth2Options.class);
final Function<JwtClaims, JwtClaims> customizeClaims = configuration.isUseJwtFormatForAccessTokens() ? claims -> {
if (claims.getIssuer() == null) {
claims.setIssuer(configuration.getJwtIssuer());
}
return claims;
} : identity();
AbstractOAuthDataProvider provider;
switch (configuration.getProvider().toLowerCase(ENGLISH)) {
case "jpa": {
if (!configuration.isAuthorizationCodeSupport()) { // else use code impl
final JPAOAuthDataProvider jpaProvider = new JPAOAuthDataProvider() {
@Override
protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
return customizeClaims.apply(super.createJwtAccessToken(at));
}
@Override
protected ServerAccessToken createNewAccessToken(final Client client, final UserSubject userSub) {
final ServerAccessToken token = super.createNewAccessToken(client, userSub);
forwardClaims(client, userSub, token);
return token;
}
};
jpaProvider.setEntityManagerFactory(JPAAdapter.createEntityManagerFactory(configuration));
provider = jpaProvider;
break;
}
}
case "jpa-code": {
final JPACodeDataProvider jpaProvider = new JPACodeDataProvider() {
@Override
protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
return customizeClaims.apply(super.createJwtAccessToken(at));
}
@Override
protected ServerAccessToken createNewAccessToken(final Client client, final UserSubject userSub) {
final ServerAccessToken token = super.createNewAccessToken(client, userSub);
forwardClaims(client, userSub, token);
return token;
}
};
jpaProvider.setEntityManagerFactory(JPAAdapter.createEntityManagerFactory(configuration));
provider = jpaProvider;
break;
}
case "jcache":
if (!configuration.isAuthorizationCodeSupport()) { // else use code impl
jCacheConfigurer.doSetup(configuration);
try {
provider = new JCacheOAuthDataProvider(configuration.getJcacheConfigUri(), bus, configuration.isJcacheStoreJwtKeyOnly()) {
@Override
protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
return customizeClaims.apply(super.createJwtAccessToken(at));
}
@Override
protected ServerAccessToken createNewAccessToken(final Client client, final UserSubject userSub) {
final ServerAccessToken token = super.createNewAccessToken(client, userSub);
forwardClaims(client, userSub, token);
return token;
}
};
} catch (final Exception e) {
throw new IllegalStateException(e);
}
break;
}
case "jcache-code":
jCacheConfigurer.doSetup(configuration);
try {
provider = new JCacheCodeDataProvider(configuration, bus) {
@Override
protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
return customizeClaims.apply(super.createJwtAccessToken(at));
}
@Override
protected ServerAccessToken createNewAccessToken(final Client client, final UserSubject userSub) {
final ServerAccessToken token = super.createNewAccessToken(client, userSub);
forwardClaims(client, userSub, token);
return token;
}
};
} catch (final Exception e) {
throw new IllegalStateException(e);
}
break;
case "encrypted":
if (!configuration.isAuthorizationCodeSupport()) { // else use code impl
provider = new DefaultEncryptingOAuthDataProvider(
new SecretKeySpec(configuration.getEncryptedKey().getBytes(StandardCharsets.UTF_8), configuration.getEncryptedAlgo())) {
@Override
protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
return customizeClaims.apply(super.createJwtAccessToken(at));
}
@Override
protected ServerAccessToken createNewAccessToken(final Client client, final UserSubject userSub) {
final ServerAccessToken token = super.createNewAccessToken(client, userSub);
forwardClaims(client, userSub, token);
return token;
}
};
break;
}
case "encrypted-code":
provider = new DefaultEncryptingCodeDataProvider(
new SecretKeySpec(configuration.getEncryptedKey().getBytes(StandardCharsets.UTF_8), configuration.getEncryptedAlgo())) {
@Override
protected JwtClaims createJwtAccessToken(final ServerAccessToken at) {
return customizeClaims.apply(super.createJwtAccessToken(at));
}
@Override
protected ServerAccessToken createNewAccessToken(final Client client, final UserSubject userSub) {
final ServerAccessToken token = super.createNewAccessToken(client, userSub);
forwardClaims(client, userSub, token);
return token;
}
};
break;
default:
throw new IllegalArgumentException("Unsupported oauth2 provider: " + configuration.getProvider());
}
final RefreshTokenGrantHandler refreshTokenGrantHandler = new RefreshTokenGrantHandler() {
@Override
public ServerAccessToken createAccessToken(final Client client,
final MultivaluedMap<String, String> params) throws OAuthServiceException {
final ServerAccessToken accessToken = super.createAccessToken(client, params);
forwardClaims(client, accessToken.getSubject(), accessToken);
return accessToken;
}
};
refreshTokenGrantHandler.setDataProvider(provider);
refreshTokenGrantHandler.setUseAllClientScopes(configuration.isUseAllClientScopes());
refreshTokenGrantHandler.setPartialMatchScopeValidation(configuration.isPartialMatchScopeValidation());
final ResourceOwnerLoginHandler loginHandler = configuration.isJaas() ? new JAASResourceOwnerLoginHandler() {
@Override
public UserSubject createSubject(final Client client, final String name, final String password) {
final UserSubject subject = super.createSubject(client, name, password);
forwardRolesAsClaims(subject);
return subject;
}
} : (client, name, password) -> {
try {
request.login(name, password);
try {
final Principal pcp = request.getUserPrincipal();
return doCreateUserSubject(pcp);
} finally {
request.logout();
}
} catch (final ServletException e) {
throw new AuthenticationException(e.getMessage());
}
};
final List<AccessTokenGrantHandler> handlers = new ArrayList<>();
handlers.add(refreshTokenGrantHandler);
handlers.add(new ClientCredentialsGrantHandler() {
@Override
protected ServerAccessToken doCreateAccessToken(final Client client,
final UserSubject subject,
final String requestedGrant,
final List<String> requestedScopes,
final List<String> audiences) {
final ServerAccessToken serverAccessToken = super.doCreateAccessToken(client, subject, requestedGrant, requestedScopes, audiences);
forwardClaims(client, subject, serverAccessToken);
return serverAccessToken;
}
});
handlers.add(new ResourceOwnerGrantHandler() {
{
setLoginHandler(loginHandler);
}
@Override
protected ServerAccessToken doCreateAccessToken(final Client client,
final UserSubject subject,
final String requestedGrant,
final List<String> requestedScopes,
final List<String> audiences) {
final ServerAccessToken serverAccessToken = super.doCreateAccessToken(client, subject, requestedGrant, requestedScopes, audiences);
forwardClaims(client, subject, serverAccessToken);
return serverAccessToken;
}
});
handlers.add(new AuthorizationCodeGrantHandler() {
@Override
public ServerAccessToken createAccessToken(final Client client, final MultivaluedMap<String, String> params) throws OAuthServiceException {
if (configuration.isUseS256CodeChallenge()) {
setCodeVerifierTransformer(new DigestCodeVerifier());
}
return super.createAccessToken(client, params);
}
@Override
protected ServerAccessToken doCreateAccessToken(final Client client,
final UserSubject subject,
final String requestedGrant,
final List<String> requestedScopes,
final List<String> audiences) {
final ServerAccessToken serverAccessToken = super.doCreateAccessToken(client, subject, requestedGrant, requestedScopes, audiences);
forwardClaims(client, subject, serverAccessToken);
return serverAccessToken;
}
});
handlers.add(new JwtBearerGrantHandler() {
@Override
protected ServerAccessToken doCreateAccessToken(final Client client,
final UserSubject subject,
final String requestedGrant,
final List<String> requestedScopes,
final List<String> audiences) {
final ServerAccessToken serverAccessToken = super.doCreateAccessToken(client, subject, requestedGrant, requestedScopes, audiences);
forwardClaims(client, subject, serverAccessToken);
return serverAccessToken;
}
});
provider.setUseJwtFormatForAccessTokens(configuration.isUseJwtFormatForAccessTokens());
provider.setAccessTokenLifetime(configuration.getAccessTokenLifetime());
provider.setRefreshTokenLifetime(configuration.getRefreshTokenLifetime());
provider.setRecycleRefreshTokens(configuration.isRecycleRefreshTokens());
provider.setSupportPreauthorizedTokens(configuration.isSupportPreauthorizedTokens());
ofNullable(configuration.getRequiredScopes()).map(s -> asList(s.split(","))).ifPresent(provider::setRequiredScopes);
ofNullable(configuration.getDefaultScopes()).map(s -> asList(s.split(","))).ifPresent(provider::setDefaultScopes);
ofNullable(configuration.getInvisibleToClientScopes()).map(s -> asList(s.split(","))).ifPresent(provider::setInvisibleToClientScopes);
ofNullable(configuration.getJwtAccessTokenClaimMap()).map(s -> new Properties() {{
try {
load(new StringReader(s));
} catch (IOException e) {
throw new IllegalArgumentException("Bad claim map configuration, use properties syntax");
}
}}).ifPresent(m -> provider.setJwtAccessTokenClaimMap(new HashMap<>(Map.class.cast(m))));
final OAuthDataProvider dataProvider;
if (configuration.isRefreshToken()) {
dataProvider = new RefreshTokenEnabledProvider(provider);
if (provider.getInvisibleToClientScopes() == null) {
provider.setInvisibleToClientScopes(new ArrayList<>());
}
provider.getInvisibleToClientScopes().add(OAuthConstants.REFRESH_TOKEN_SCOPE);
} else {
dataProvider = provider;
}
handlers.stream()
.filter(AbstractGrantHandler.class::isInstance)
.forEach(h -> {
final AbstractGrantHandler handler = AbstractGrantHandler.class.cast(h);
handler.setDataProvider(dataProvider);
handler.setCanSupportPublicClients(configuration.isCanSupportPublicClients());
handler.setPartialMatchScopeValidation(configuration.isPartialMatchScopeValidation());
});
abstractTokenServiceConsumer = s -> { // this is used @RequestScoped so ensure it is not slow for no reason
s.setCanSupportPublicClients(configuration.isCanSupportPublicClients());
s.setBlockUnsecureRequests(configuration.isBlockUnsecureRequests());
s.setWriteCustomErrors(configuration.isWriteCustomErrors());
s.setWriteOptionalParameters(configuration.isWriteOptionalParameters());
s.setDataProvider(dataProvider);
};
tokenServiceConsumer = s -> { // this is used @RequestScoped so ensure it is not slow for no reason
abstractTokenServiceConsumer.accept(s);
s.setGrantHandlers(handlers);
};
final List<String> noConsentScopes = ofNullable(configuration.getScopesRequiringNoConsent())
.map(s -> asList(s.split(",")))
.orElse(null);
// we prefix them oauth2.cxf. but otherwise it is the plain cxf config
securityProperties = ofNullable(builder.getProperties()).map(Properties::stringPropertyNames).orElse(emptySet()).stream()
.filter(s -> s.startsWith("oauth2.cxf.rs.security."))
.collect(toMap(s -> s.substring("oauth2.cxf.".length()), s -> builder.getProperties().getProperty(s)));
final JoseSessionTokenProvider sessionAuthenticityTokenProvider = new JoseSessionTokenProvider() {
@Override
public String createSessionToken(final MessageContext mc, final MultivaluedMap<String, String> params,
final UserSubject subject, final OAuthRedirectionState secData) {
secData.setClientCodeChallenge(params.getFirst(OAuthConstants.AUTHORIZATION_CODE_CHALLENGE)); // CXF-8368
return super.createSessionToken(mc, params, subject, secData);
}
};
sessionAuthenticityTokenProvider.setMaxDefaultSessionInterval(configuration.getMaxDefaultSessionInterval());
// TODO: other configs
redirectionBasedGrantServiceConsumer = s -> {
s.setDataProvider(dataProvider);
s.setBlockUnsecureRequests(configuration.isBlockUnsecureRequests());
s.setWriteOptionalParameters(configuration.isWriteOptionalParameters());
s.setUseAllClientScopes(configuration.isUseAllClientScopes());
s.setPartialMatchScopeValidation(configuration.isPartialMatchScopeValidation());
s.setUseRegisteredRedirectUriIfPossible(configuration.isUseRegisteredRedirectUriIfPossible());
s.setMaxDefaultSessionInterval(configuration.getMaxDefaultSessionInterval());
s.setMatchRedirectUriWithApplicationUri(configuration.isMatchRedirectUriWithApplicationUri());
s.setScopesRequiringNoConsent(noConsentScopes);
s.setSessionAuthenticityTokenProvider(sessionAuthenticityTokenProvider);
s.setCanSupportPublicClients(configuration.isCanSupportPublicClients());
};
}
public UserSubject doCreateUserSubject(final Principal pcp) {
final List<String> roles = GenericPrincipal.class.isInstance(pcp) ?
new ArrayList<>(asList(GenericPrincipal.class.cast(pcp).getRoles())) : Collections.<String>emptyList();
final String name = pcp.getName();
final UserSubject userSubject = new UserSubject(name, name, roles);
final Message m = JAXRSUtils.getCurrentMessage();
if (m != null && m.get(AuthenticationMethod.class) != null) {
userSubject.setAuthenticationMethod(m.get(AuthenticationMethod.class));
} else {
userSubject.setAuthenticationMethod(PASSWORD);
}
forwardRolesAsClaims(userSubject);
return userSubject;
}
protected void forwardRolesAsClaims(final UserSubject subject) {
if (configuration.isForwardRoleAsJwtClaims() && subject.getRoles() != null) {
subject.setProperties(new HashMap<>());
subject.getProperties().put("claim.roles", String.join(", ", subject.getRoles()));
}
}
protected void forwardClaims(final Client client, final UserSubject subject,
final ServerAccessToken serverAccessToken) {
forwardClientClaims(client, serverAccessToken);
forwardUserClaims(subject, serverAccessToken);
}
protected void forwardUserClaims(final UserSubject subject,
final ServerAccessToken serverAccessToken) {
if (subject.getProperties() == null || subject.getProperties().isEmpty()) {
return;
}
final Map<String, String> claims = subject.getProperties().entrySet().stream()
.filter(it -> it.getKey().startsWith("claim."))
.collect(toMap(it -> it.getKey().substring("claim.".length()), Map.Entry::getValue));
if (claims.isEmpty()) {
return;
}
if (serverAccessToken.getExtraProperties() == null) {
serverAccessToken.setExtraProperties(claims);
} else {
serverAccessToken.getExtraProperties().putAll(claims);
}
}
private void forwardClientClaims(final Client client, final ServerAccessToken serverAccessToken) {
if (client.getProperties() == null || client.getProperties().isEmpty()) {
return;
}
final Map<String, String> claims = client.getProperties().entrySet().stream()
.filter(it -> it.getKey().startsWith("claim."))
.collect(toMap(it -> it.getKey().substring("claim.".length()), Map.Entry::getValue));
if (claims.isEmpty()) {
return;
}
if (serverAccessToken.getExtraProperties() == null) {
serverAccessToken.setExtraProperties(claims);
} else {
serverAccessToken.getExtraProperties().putAll(claims);
}
}
private void forwardSecurityProperties() {
// TODO: make it even more contextual, client based?
final Message currentMessage = PhaseInterceptorChain.getCurrentMessage();
securityProperties.forEach(currentMessage::put);
}
public void accept(final AbstractTokenService service) {
abstractTokenServiceConsumer.accept(service);
}
public void accept(final AccessTokenService service) {
tokenServiceConsumer.accept(service);
forwardSecurityProperties();
}
public void accept(final AuthorizationCodeGrantService service) {
redirectionBasedGrantServiceConsumer.accept(service);
forwardSecurityProperties();
}
public OAuth2Options getConfiguration() {
return configuration;
}
}