OAuth2ResourceServerBeanDefinitionParser.java
/*
* Copyright 2004-present the original author or authors.
*
* Licensed 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
*
* https://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.springframework.security.config.http;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.config.Elements;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
/**
* A {@link BeanDefinitionParser} for <http>'s <oauth2-resource-server>
* element.
*
* @author Josh Cummings
* @since 5.3
*/
final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionParser {
static final String AUTHENTICATION_MANAGER_RESOLVER_REF = "authentication-manager-resolver-ref";
static final String BEARER_TOKEN_RESOLVER_REF = "bearer-token-resolver-ref";
static final String AUTHENTICATION_CONVERTER_REF = "authentication-converter-ref";
static final String ENTRY_POINT_REF = "entry-point-ref";
static final String BEARER_TOKEN_RESOLVER = "bearerTokenResolver";
static final String AUTHENTICATION_ENTRY_POINT = "authenticationEntryPoint";
private final BeanReference authenticationManager;
private final List<BeanReference> authenticationProviders;
private final Map<BeanDefinition, BeanMetadataElement> entryPoints;
private final Map<BeanDefinition, BeanMetadataElement> deniedHandlers;
private final List<BeanDefinition> ignoreCsrfRequestMatchers;
private final BeanDefinition authenticationEntryPoint = new RootBeanDefinition(
BearerTokenAuthenticationEntryPoint.class);
private final BeanDefinition accessDeniedHandler = new RootBeanDefinition(BearerTokenAccessDeniedHandler.class);
private final BeanMetadataElement authenticationFilterSecurityContextHolderStrategy;
OAuth2ResourceServerBeanDefinitionParser(BeanReference authenticationManager,
List<BeanReference> authenticationProviders, Map<BeanDefinition, BeanMetadataElement> entryPoints,
Map<BeanDefinition, BeanMetadataElement> deniedHandlers, List<BeanDefinition> ignoreCsrfRequestMatchers,
BeanMetadataElement authenticationFilterSecurityContextHolderStrategy) {
this.authenticationManager = authenticationManager;
this.authenticationProviders = authenticationProviders;
this.entryPoints = entryPoints;
this.deniedHandlers = deniedHandlers;
this.ignoreCsrfRequestMatchers = ignoreCsrfRequestMatchers;
this.authenticationFilterSecurityContextHolderStrategy = authenticationFilterSecurityContextHolderStrategy;
}
/**
* Parse a <oauth2-resource-server> element and return the corresponding
* {@link BearerTokenAuthenticationFilter}
* @param oauth2ResourceServer the <oauth2-resource-server> element.
* @param pc the {@link ParserContext}
* @return a {@link BeanDefinition} representing a
* {@link BearerTokenAuthenticationFilter} definition
*/
@Override
public BeanDefinition parse(Element oauth2ResourceServer, ParserContext pc) {
Element jwt = DomUtils.getChildElementByTagName(oauth2ResourceServer, Elements.JWT);
Element opaqueToken = DomUtils.getChildElementByTagName(oauth2ResourceServer, Elements.OPAQUE_TOKEN);
validateConfiguration(oauth2ResourceServer, jwt, opaqueToken, pc);
if (jwt != null) {
BeanDefinition jwtAuthenticationProvider = new JwtBeanDefinitionParser().parse(jwt, pc);
this.authenticationProviders.add(new RuntimeBeanReference(
pc.getReaderContext().registerWithGeneratedName(jwtAuthenticationProvider)));
}
if (opaqueToken != null) {
BeanDefinition opaqueTokenAuthenticationProvider = new OpaqueTokenBeanDefinitionParser().parse(opaqueToken,
pc);
this.authenticationProviders.add(new RuntimeBeanReference(
pc.getReaderContext().registerWithGeneratedName(opaqueTokenAuthenticationProvider)));
}
BeanMetadataElement bearerTokenResolver = getBearerTokenResolver(oauth2ResourceServer);
BeanMetadataElement authenticationConverter = getAuthenticationConverter(oauth2ResourceServer);
if (bearerTokenResolver != null && authenticationConverter != null) {
throw new BeanDefinitionStoreException(
"You cannot use bearer-token-ref and authentication-converter-ref in the same oauth2-resource-server element");
}
if (bearerTokenResolver == null && authenticationConverter == null) {
authenticationConverter = new RootBeanDefinition(BearerTokenAuthenticationConverter.class);
}
BeanMetadataElement authenticationEntryPoint = getEntryPoint(oauth2ResourceServer);
BeanDefinition requestMatcher = buildRequestMatcher(bearerTokenResolver, authenticationConverter);
this.entryPoints.put(requestMatcher, authenticationEntryPoint);
this.deniedHandlers.put(requestMatcher, this.accessDeniedHandler);
this.ignoreCsrfRequestMatchers.add(requestMatcher);
BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder
.rootBeanDefinition(BearerTokenAuthenticationFilter.class);
BeanMetadataElement authenticationManagerResolver = getAuthenticationManagerResolver(oauth2ResourceServer);
filterBuilder.addConstructorArgValue(authenticationManagerResolver);
filterBuilder.addPropertyValue(AUTHENTICATION_ENTRY_POINT, authenticationEntryPoint);
filterBuilder.addPropertyValue("securityContextHolderStrategy",
this.authenticationFilterSecurityContextHolderStrategy);
if (authenticationConverter != null) {
filterBuilder.addConstructorArgValue(authenticationConverter);
}
if (bearerTokenResolver != null) {
filterBuilder.addPropertyValue(BEARER_TOKEN_RESOLVER, bearerTokenResolver);
}
return filterBuilder.getBeanDefinition();
}
private BeanDefinition buildRequestMatcher(BeanMetadataElement bearerTokenResolver,
BeanMetadataElement authenticationConverter) {
if (bearerTokenResolver != null) {
BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
.rootBeanDefinition(BearerTokenRequestMatcher.class);
requestMatcherBuilder.addConstructorArgValue(bearerTokenResolver);
return requestMatcherBuilder.getBeanDefinition();
}
BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
.rootBeanDefinition(BearerTokenAuthenticationRequestMatcher.class);
if (authenticationConverter != null) {
requestMatcherBuilder.addConstructorArgValue(authenticationConverter);
}
return requestMatcherBuilder.getBeanDefinition();
}
void validateConfiguration(Element oauth2ResourceServer, Element jwt, Element opaqueToken, ParserContext pc) {
if (!oauth2ResourceServer.hasAttribute(AUTHENTICATION_MANAGER_RESOLVER_REF)) {
if (jwt == null && opaqueToken == null) {
pc.getReaderContext()
.error("Didn't find authentication-manager-resolver-ref, " + "<jwt>, or <opaque-token>. "
+ "Please select one.", oauth2ResourceServer);
}
return;
}
if (jwt != null) {
pc.getReaderContext()
.error("Found <jwt> as well as authentication-manager-resolver-ref. Please select just one.",
oauth2ResourceServer);
}
if (opaqueToken != null) {
pc.getReaderContext()
.error("Found <opaque-token> as well as authentication-manager-resolver-ref. Please select just one.",
oauth2ResourceServer);
}
}
BeanMetadataElement getAuthenticationManagerResolver(Element element) {
String authenticationManagerResolverRef = element.getAttribute(AUTHENTICATION_MANAGER_RESOLVER_REF);
if (StringUtils.hasLength(authenticationManagerResolverRef)) {
return new RuntimeBeanReference(authenticationManagerResolverRef);
}
BeanDefinitionBuilder authenticationManagerResolver = BeanDefinitionBuilder
.rootBeanDefinition(StaticAuthenticationManagerResolver.class);
authenticationManagerResolver.addConstructorArgValue(this.authenticationManager);
return authenticationManagerResolver.getBeanDefinition();
}
BeanMetadataElement getBearerTokenResolver(Element element) {
String bearerTokenResolverRef = element.getAttribute(BEARER_TOKEN_RESOLVER_REF);
if (!StringUtils.hasLength(bearerTokenResolverRef)) {
return null;
}
return new RuntimeBeanReference(bearerTokenResolverRef);
}
BeanMetadataElement getAuthenticationConverter(Element element) {
String authenticationConverterRef = element.getAttribute(AUTHENTICATION_CONVERTER_REF);
if (!StringUtils.hasLength(authenticationConverterRef)) {
return null;
}
return new RuntimeBeanReference(authenticationConverterRef);
}
BeanMetadataElement getEntryPoint(Element element) {
String entryPointRef = element.getAttribute(ENTRY_POINT_REF);
if (!StringUtils.hasLength(entryPointRef)) {
return this.authenticationEntryPoint;
}
return new RuntimeBeanReference(entryPointRef);
}
static final class JwtBeanDefinitionParser implements BeanDefinitionParser {
static final String DECODER_REF = "decoder-ref";
static final String JWK_SET_URI = "jwk-set-uri";
static final String JWT_AUTHENTICATION_CONVERTER_REF = "jwt-authentication-converter-ref";
static final String JWT_AUTHENTICATION_CONVERTER = "jwtAuthenticationConverter";
JwtBeanDefinitionParser() {
}
@Override
public BeanDefinition parse(Element element, ParserContext pc) {
validateConfiguration(element, pc);
BeanDefinitionBuilder jwtProviderBuilder = BeanDefinitionBuilder
.rootBeanDefinition(JwtAuthenticationProvider.class);
jwtProviderBuilder.addConstructorArgValue(getDecoder(element));
jwtProviderBuilder.addPropertyValue(JWT_AUTHENTICATION_CONVERTER, getJwtAuthenticationConverter(element));
return jwtProviderBuilder.getBeanDefinition();
}
void validateConfiguration(Element element, ParserContext pc) {
boolean usesDecoder = element.hasAttribute(DECODER_REF);
boolean usesJwkSetUri = element.hasAttribute(JWK_SET_URI);
if (usesDecoder == usesJwkSetUri) {
pc.getReaderContext().error("Please specify either decoder-ref or jwk-set-uri.", element);
}
}
Object getDecoder(Element element) {
String decoderRef = element.getAttribute(DECODER_REF);
if (StringUtils.hasLength(decoderRef)) {
return new RuntimeBeanReference(decoderRef);
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.rootBeanDefinition(NimbusJwtDecoderJwkSetUriFactoryBean.class);
builder.addConstructorArgValue(element.getAttribute(JWK_SET_URI));
return builder.getBeanDefinition();
}
Object getJwtAuthenticationConverter(Element element) {
String jwtDecoderRef = element.getAttribute(JWT_AUTHENTICATION_CONVERTER_REF);
return (StringUtils.hasLength(jwtDecoderRef)) ? new RuntimeBeanReference(jwtDecoderRef)
: new JwtAuthenticationConverter();
}
}
static final class OpaqueTokenBeanDefinitionParser implements BeanDefinitionParser {
static final String INTROSPECTOR_REF = "introspector-ref";
static final String INTROSPECTION_URI = "introspection-uri";
static final String CLIENT_ID = "client-id";
static final String CLIENT_SECRET = "client-secret";
static final String AUTHENTICATION_CONVERTER_REF = "authentication-converter-ref";
static final String AUTHENTICATION_CONVERTER = "authenticationConverter";
OpaqueTokenBeanDefinitionParser() {
}
@Override
public BeanDefinition parse(Element element, ParserContext pc) {
validateConfiguration(element, pc);
BeanMetadataElement introspector = getIntrospector(element);
String authenticationConverterRef = element.getAttribute(AUTHENTICATION_CONVERTER_REF);
BeanDefinitionBuilder opaqueTokenProviderBuilder = BeanDefinitionBuilder
.rootBeanDefinition(OpaqueTokenAuthenticationProvider.class);
opaqueTokenProviderBuilder.addConstructorArgValue(introspector);
if (StringUtils.hasText(authenticationConverterRef)) {
opaqueTokenProviderBuilder.addPropertyReference(AUTHENTICATION_CONVERTER, authenticationConverterRef);
}
return opaqueTokenProviderBuilder.getBeanDefinition();
}
void validateConfiguration(Element element, ParserContext pc) {
boolean usesIntrospector = element.hasAttribute(INTROSPECTOR_REF);
boolean usesEndpoint = element.hasAttribute(INTROSPECTION_URI) || element.hasAttribute(CLIENT_ID)
|| element.hasAttribute(CLIENT_SECRET);
if (usesIntrospector == usesEndpoint) {
pc.getReaderContext()
.error("Please specify either introspector-ref or all of "
+ "introspection-uri, client-id, and client-secret.", element);
return;
}
if (usesEndpoint) {
if (!(element.hasAttribute(INTROSPECTION_URI) && element.hasAttribute(CLIENT_ID)
&& element.hasAttribute(CLIENT_SECRET))) {
pc.getReaderContext()
.error("Please specify introspection-uri, client-id, and client-secret together", element);
}
}
}
BeanMetadataElement getIntrospector(Element element) {
String introspectorRef = element.getAttribute(INTROSPECTOR_REF);
if (StringUtils.hasLength(introspectorRef)) {
return new RuntimeBeanReference(introspectorRef);
}
String introspectionUri = element.getAttribute(INTROSPECTION_URI);
String clientId = element.getAttribute(CLIENT_ID);
String clientSecret = element.getAttribute(CLIENT_SECRET);
BeanDefinitionBuilder introspectorBuilder = BeanDefinitionBuilder
.rootBeanDefinition(SpringOpaqueTokenIntrospector.class);
introspectorBuilder.addConstructorArgValue(introspectionUri);
introspectorBuilder.addConstructorArgValue(clientId);
introspectorBuilder.addConstructorArgValue(clientSecret);
return introspectorBuilder.getBeanDefinition();
}
}
static final class StaticAuthenticationManagerResolver
implements AuthenticationManagerResolver<HttpServletRequest> {
private final AuthenticationManager authenticationManager;
StaticAuthenticationManagerResolver(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public AuthenticationManager resolve(HttpServletRequest context) {
return this.authenticationManager;
}
}
static final class NimbusJwtDecoderJwkSetUriFactoryBean implements FactoryBean<JwtDecoder> {
private final String jwkSetUri;
NimbusJwtDecoderJwkSetUriFactoryBean(String jwkSetUri) {
this.jwkSetUri = jwkSetUri;
}
@Override
public JwtDecoder getObject() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
}
@Override
public Class<?> getObjectType() {
return JwtDecoder.class;
}
}
static final class BearerTokenRequestMatcher implements RequestMatcher {
private final BearerTokenResolver bearerTokenResolver;
BearerTokenRequestMatcher(BearerTokenResolver bearerTokenResolver) {
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
this.bearerTokenResolver = bearerTokenResolver;
}
@Override
public boolean matches(HttpServletRequest request) {
try {
return this.bearerTokenResolver.resolve(request) != null;
}
catch (OAuth2AuthenticationException ex) {
return false;
}
}
}
static final class BearerTokenAuthenticationRequestMatcher implements RequestMatcher {
private final AuthenticationConverter authenticationConverter;
BearerTokenAuthenticationRequestMatcher() {
this.authenticationConverter = new BearerTokenAuthenticationConverter();
}
BearerTokenAuthenticationRequestMatcher(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
@Override
public boolean matches(HttpServletRequest request) {
try {
return this.authenticationConverter.convert(request) != null;
}
catch (OAuth2AuthenticationException ex) {
return false;
}
}
}
}