From 71a5c9521c2b0de57da6870737ed4bccd5ffc523 Mon Sep 17 00:00:00 2001 From: Ruby Hartono <58564005+rh-id@users.noreply.github.com> Date: Tue, 24 Dec 2019 22:10:46 +0800 Subject: [PATCH] Add XML namespace support for oauth2-login Fixes gh-4557 --- .../security/config/Elements.java | 2 +- .../config/SecurityNamespaceHandler.java | 2 +- .../http/AuthenticationConfigBuilder.java | 89 ++- .../HttpSecurityBeanDefinitionParser.java | 5 +- .../http/OAuth2LoginBeanDefinitionParser.java | 460 +++++++++++++- .../security/config/http/SecurityFilters.java | 4 +- ...ientRegistrationsBeanDefinitionParser.java | 207 +++++- .../security/config/spring-security-5.3.rnc | 50 +- .../security/config/spring-security-5.3.xsd | 96 ++- .../config/doc/XsdDocumentedTests.java | 2 +- .../OAuth2LoginBeanDefinitionParserTests.java | 599 +++++++++++++++++- ...egistrationsBeanDefinitionParserTests.java | 242 +++++++ .../util/InMemoryXmlApplicationContext.java | 2 +- ...ionParserTests-MultiClientRegistration.xml | 34 + ...WithCustomAuthenticationFailureHandler.xml | 38 ++ ...ration-WithCustomAuthenticationHandler.xml | 53 ++ ...WithCustomAuthorizationRequestResolver.xml | 38 ++ ...istration-WithCustomGrantedAuthorities.xml | 58 ++ ...ClientRegistration-WithCustomLoginPage.xml | 34 + ...istration-WithCustomLoginProcessingUrl.xml | 51 ++ ...SingleClientRegistration-WithFormLogin.xml | 35 + ...ientRegistration-WithJwtDecoderFactory.xml | 54 ++ ...DecoderFactoryAndDefaultSuccessHandler.xml | 51 ++ ...ientRegistration-WithTestConfiguration.xml | 57 ++ ...ingleClientRegistration-WithinSameFile.xml | 52 ++ ...onParserTests-SingleClientRegistration.xml | 30 +- ...thCustomAuthorizationRequestRepository.xml | 49 ++ ...s-WithCustomAuthorizedClientRepository.xml | 53 ++ ...ests-WithCustomAuthorizedClientService.xml | 53 ++ ...WithCustomClientRegistrationRepository.xml | 50 ++ ...eanDefinitionParserTests-FromIssuerUri.xml | 39 ++ ...ionParserTests-MultiClientRegistration.xml | 27 + .../client/google-github-registration.xml | 59 ++ .../oauth2/client/google-registration.xml | 44 ++ .../_includes/servlet/appendix/namespace.adoc | 73 +++ 35 files changed, 2729 insertions(+), 63 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationFailureHandler.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationHandler.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRequestResolver.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomGrantedAuthorities.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginPage.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginProcessingUrl.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithFormLogin.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactory.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithTestConfiguration.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithinSameFile.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizationRequestRepository.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientRepository.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientService.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomClientRegistrationRepository.xml create mode 100644 config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-FromIssuerUri.xml create mode 100644 config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-MultiClientRegistration.xml create mode 100644 config/src/test/resources/org/springframework/security/config/oauth2/client/google-github-registration.xml create mode 100644 config/src/test/resources/org/springframework/security/config/oauth2/client/google-registration.xml diff --git a/config/src/main/java/org/springframework/security/config/Elements.java b/config/src/main/java/org/springframework/security/config/Elements.java index b21bcceecf..6684c67c0d 100644 --- a/config/src/main/java/org/springframework/security/config/Elements.java +++ b/config/src/main/java/org/springframework/security/config/Elements.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. diff --git a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java index 59bb8a77c8..d3bb38032b 100644 --- a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2019 the original author or authors. + * Copyright 2009-2020 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. diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 40690138bb..9a89bff873 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -138,6 +138,15 @@ final class AuthenticationConfigBuilder { private String openIDLoginPage; + private String oauth2LoginFilterId; + private String oauth2AuthorizationRequestRedirectFilterId; + private BeanDefinition oauth2AuthorizationRequestRedirectFilter; + private BeanDefinition oauth2LoginEntryPoint; + private BeanReference oauth2LoginAuthenticationProviderRef; + private BeanReference oauth2LoginOidcAuthenticationProviderRef; + + private BeanDefinition oauth2LoginLinks; + AuthenticationConfigBuilder(Element element, boolean forceAutoConfig, ParserContext pc, SessionCreationPolicy sessionPolicy, BeanReference requestCache, BeanReference authenticationManager, @@ -238,10 +247,42 @@ final class AuthenticationConfigBuilder { void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager) { Element oauth2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_LOGIN); - if (oauth2LoginElt != null || this.autoConfig) { - OAuth2LoginBeanDefinitionParser parser = new OAuth2LoginBeanDefinitionParser(); - parser.parse(oauth2LoginElt, this.pc); - // TODO Implement + if (oauth2LoginElt != null) { + OAuth2LoginBeanDefinitionParser parser = new OAuth2LoginBeanDefinitionParser(requestCache, portMapper, + portResolver, sessionStrategy, allowSessionCreation); + BeanDefinition oauth2LoginFilterBean = parser.parse(oauth2LoginElt, this.pc); + oauth2LoginFilterBean.getPropertyValues().addPropertyValue("authenticationManager", authManager); + + // retrieve the other bean result + BeanDefinition oauth2LoginAuthProvider = parser.getOAuth2LoginAuthenticationProvider(); + oauth2AuthorizationRequestRedirectFilter = parser.getOAuth2AuthorizationRequestRedirectFilter(); + oauth2LoginEntryPoint = parser.getOAuth2LoginAuthenticationEntryPoint(); + + // generate bean name to be registered + String oauth2LoginAuthenticationProviderId = pc.getReaderContext() + .generateBeanName(oauth2LoginAuthProvider); + oauth2LoginFilterId = pc.getReaderContext().generateBeanName(oauth2LoginFilterBean); + oauth2AuthorizationRequestRedirectFilterId = pc.getReaderContext() + .generateBeanName(oauth2AuthorizationRequestRedirectFilter); + oauth2LoginLinks = parser.getOAuth2LoginLinks(); + + // register the component + pc.registerBeanComponent(new BeanComponentDefinition(oauth2AuthorizationRequestRedirectFilter, + oauth2AuthorizationRequestRedirectFilterId)); + pc.registerBeanComponent(new BeanComponentDefinition(oauth2LoginFilterBean, oauth2LoginFilterId)); + pc.registerBeanComponent( + new BeanComponentDefinition(oauth2LoginAuthProvider, oauth2LoginAuthenticationProviderId)); + + oauth2LoginAuthenticationProviderRef = new RuntimeBeanReference(oauth2LoginAuthenticationProviderId); + + // oidc provider + BeanDefinition oauth2LoginOidcAuthProvider = parser.getOAuth2LoginOidcAuthenticationProvider(); + String oauth2LoginOidcAuthenticationProviderId = pc.getReaderContext() + .generateBeanName(oauth2LoginOidcAuthProvider); + pc.registerBeanComponent( + new BeanComponentDefinition(oauth2LoginOidcAuthProvider, oauth2LoginOidcAuthenticationProviderId)); + oauth2LoginOidcAuthenticationProviderRef = new RuntimeBeanReference( + oauth2LoginOidcAuthenticationProviderId); } } @@ -545,7 +586,7 @@ final class AuthenticationConfigBuilder { } void createLoginPageFilterIfNeeded() { - boolean needLoginPage = formFilterId != null || openIDFilterId != null; + boolean needLoginPage = formFilterId != null || openIDFilterId != null || oauth2LoginFilterId != null; // If no login page has been defined, add in the default page generator. if (needLoginPage && formLoginPage == null && openIDLoginPage == null) { @@ -571,6 +612,12 @@ final class AuthenticationConfigBuilder { openidLoginProcessingUrl); } + if (oauth2LoginFilterId != null) { + loginPageFilter.addConstructorArgReference(oauth2LoginFilterId); + loginPageFilter.addPropertyValue("Oauth2LoginEnabled", true); + loginPageFilter.addPropertyValue("Oauth2AuthenticationUrlToClientName", oauth2LoginLinks); + } + loginPageGenerationFilter = loginPageFilter.getBeanDefinition(); this.logoutPageGenerationFilter = logoutPageFilter.getBeanDefinition(); } @@ -732,7 +779,7 @@ final class AuthenticationConfigBuilder { Element openIDLoginElt = DomUtils.getChildElementByTagName(httpElt, Elements.OPENID_LOGIN); // Basic takes precedence if explicit element is used and no others are configured - if (basicAuthElt != null && formLoginElt == null && openIDLoginElt == null) { + if (basicAuthElt != null && formLoginElt == null && openIDLoginElt == null && oauth2LoginEntryPoint == null) { return basicEntryPoint; } @@ -747,7 +794,15 @@ final class AuthenticationConfigBuilder { } if (formFilterId != null && openIDLoginPage == null) { - return formEntryPoint; + // gh-6802 + // If form login was enabled through element and Oauth2 login was enabled from element then use form login + if (formLoginElt != null && oauth2LoginEntryPoint != null) { + return formEntryPoint; + } + // If form login was enabled through auto-config, and Oauth2 login was not enabled then use form login + if (oauth2LoginEntryPoint == null) { + return formEntryPoint; + } } // Otherwise use OpenID if enabled @@ -760,6 +815,11 @@ final class AuthenticationConfigBuilder { return preAuthEntryPoint; } + // OAuth2 entry point will not be null if only 1 client registration + if (oauth2LoginEntryPoint != null) { + return oauth2LoginEntryPoint; + } + pc.getReaderContext() .error("No AuthenticationEntryPoint could be established. Please " + "make sure you have a login mechanism configured through the namespace (such as form-login) or " @@ -808,6 +868,11 @@ final class AuthenticationConfigBuilder { FORM_LOGIN_FILTER)); } + if (oauth2LoginFilterId != null) { + filters.add(new OrderDecorator(new RuntimeBeanReference(oauth2LoginFilterId), OAUTH2_LOGIN_FILTER)); + filters.add(new OrderDecorator(oauth2AuthorizationRequestRedirectFilter, OAUTH2_REDIRECT_FILTER)); + } + if (openIDFilterId != null) { filters.add(new OrderDecorator(new RuntimeBeanReference(openIDFilterId), OPENID_FILTER)); @@ -850,6 +915,14 @@ final class AuthenticationConfigBuilder { providers.add(jeeProviderRef); } + if (oauth2LoginAuthenticationProviderRef != null) { + providers.add(oauth2LoginAuthenticationProviderRef); + } + + if (oauth2LoginOidcAuthenticationProviderRef != null) { + providers.add(oauth2LoginOidcAuthenticationProviderRef); + } + return providers; } diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java index f48da35f18..ccb1abf7ae 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2020 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. @@ -33,6 +33,7 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.BeanIds; import org.springframework.security.config.Elements; @@ -294,6 +295,8 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { clearCredentials); } + // gh-6009 + authManager.addPropertyValue("authenticationEventPublisher", new RootBeanDefinition(DefaultAuthenticationEventPublisher.class)); authManager.getRawBeanDefinition().setSource(pc.extractSource(element)); BeanDefinition authMgrBean = authManager.getBeanDefinition(); String id = pc.getReaderContext().generateBeanName(authMgrBean); diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java index ec9ae395e7..255b0083b3 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -15,19 +15,471 @@ */ package org.springframework.security.config.http; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.BeansException; 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.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.Elements; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; import org.w3c.dom.Element; /** - * @author + * @author Ruby Hartono + * @since 5.3 */ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { + private static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization"; + private static final String DEFAULT_LOGIN_URI = DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL; + + private static final String ELT_CLIENT_REGISTRATION = "client-registration"; + private static final String ATT_REGISTRATION_ID = "registration-id"; + private static final String ATT_CLIENT_REGISTRATION_REPOSITORY_REF = "client-registration-repository-ref"; + private static final String ATT_AUTHORIZED_CLIENT_REPOSITORY_REF = "authorized-client-repository-ref"; + private static final String ATT_AUTHORIZED_CLIENT_SERVICE_REF = "authorized-client-service-ref"; + private static final String ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF = "authorization-request-repository-ref"; + private static final String ATT_AUTHORIZATION_REQUEST_RESOLVER_REF = "authorization-request-resolver-ref"; + private static final String ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF = "access-token-response-client-ref"; + private static final String ATT_USER_AUTHORITIES_MAPPER_REF = "user-authorities-mapper-ref"; + private static final String ATT_USER_SERVICE_REF = "user-service-ref"; + private static final String ATT_OIDC_USER_SERVICE_REF = "oidc-user-service-ref"; + private static final String ATT_LOGIN_PROCESSING_URL = "login-processing-url"; + private static final String ATT_LOGIN_PAGE = "login-page"; + private static final String ATT_AUTHENTICATION_SUCCESS_HANDLER_REF = "authentication-success-handler-ref"; + private static final String ATT_AUTHENTICATION_FAILURE_HANDLER_REF = "authentication-failure-handler-ref"; + private static final String ATT_JWT_DECODER_FACTORY_REF = "jwt-decoder-factory-ref"; + + private BeanReference requestCache; + private final BeanReference portMapper; + private final BeanReference portResolver; + private final BeanReference sessionStrategy; + private final boolean allowSessionCreation; + + private BeanDefinition oauth2AuthorizationRequestRedirectFilter; + + private BeanDefinition oauth2LoginAuthenticationEntryPoint; + + private BeanDefinition oauth2LoginAuthenticationProvider; + + private BeanDefinition oauth2LoginOidcAuthenticationProvider; + + private BeanDefinition oauth2LoginLinks; + + OAuth2LoginBeanDefinitionParser(BeanReference requestCache, BeanReference portMapper, BeanReference portResolver, + BeanReference sessionStrategy, boolean allowSessionCreation) { + this.requestCache = requestCache; + this.portMapper = portMapper; + this.portResolver = portResolver; + this.sessionStrategy = sessionStrategy; + this.allowSessionCreation = allowSessionCreation; + } + @Override public BeanDefinition parse(Element element, ParserContext parserContext) { - // TODO Implement - return null; + // register magic bean + BeanDefinition oauth2LoginBeanConfig = BeanDefinitionBuilder.rootBeanDefinition(OAuth2LoginBeanConfig.class) + .getBeanDefinition(); + String oauth2LoginBeanConfigId = parserContext.getReaderContext().generateBeanName(oauth2LoginBeanConfig); + parserContext + .registerBeanComponent(new BeanComponentDefinition(oauth2LoginBeanConfig, oauth2LoginBeanConfigId)); + + // configure filter + BeanMetadataElement clientRegistrationRepository = getClientRegistrationRepository(element); + BeanMetadataElement authorizedClientRepository = getAuthorizedClientRepository(element, + clientRegistrationRepository); + BeanMetadataElement accessTokenResponseClient = getAccessTokenResponseClient(element); + BeanMetadataElement oauth2UserService = getOAuth2UserService(element); + BeanMetadataElement oauth2AuthRequestRepository = getOAuth2AuthorizationRequestRepository(element); + + BeanDefinitionBuilder oauth2LoginAuthenticationFilterBuilder = BeanDefinitionBuilder + .rootBeanDefinition(OAuth2LoginAuthenticationFilter.class) + .addConstructorArgValue(clientRegistrationRepository).addConstructorArgValue(authorizedClientRepository) + .addPropertyValue("authorizationRequestRepository", oauth2AuthRequestRepository); + + if (sessionStrategy != null) { + oauth2LoginAuthenticationFilterBuilder.addPropertyValue("sessionAuthenticationStrategy", sessionStrategy); + } + + Object source = parserContext.extractSource(element); + String loginProcessingUrl = element.getAttribute(ATT_LOGIN_PROCESSING_URL); + WebConfigUtils.validateHttpRedirect(loginProcessingUrl, parserContext, source); + if (!StringUtils.isEmpty(loginProcessingUrl)) { + oauth2LoginAuthenticationFilterBuilder.addConstructorArgValue(loginProcessingUrl); + } else { + oauth2LoginAuthenticationFilterBuilder + .addConstructorArgValue(OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); + } + + BeanDefinitionBuilder oauth2LoginAuthenticationProviderBuilder = BeanDefinitionBuilder + .rootBeanDefinition(OAuth2LoginAuthenticationProvider.class) + .addConstructorArgValue(accessTokenResponseClient).addConstructorArgValue(oauth2UserService); + + String oauth2UserAuthMapperRef = element.getAttribute(ATT_USER_AUTHORITIES_MAPPER_REF); + if (!StringUtils.isEmpty(oauth2UserAuthMapperRef)) { + oauth2LoginAuthenticationProviderBuilder.addPropertyReference("authoritiesMapper", oauth2UserAuthMapperRef); + } + + oauth2LoginAuthenticationProvider = oauth2LoginAuthenticationProviderBuilder.getBeanDefinition(); + + oauth2LoginOidcAuthenticationProvider = getOAuth2OidcAuthProvider(element, accessTokenResponseClient, + oauth2UserAuthMapperRef, parserContext); + + BeanDefinitionBuilder oauth2AuthorizationRequestRedirectFilterBuilder = BeanDefinitionBuilder + .rootBeanDefinition(OAuth2AuthorizationRequestRedirectFilter.class); + + String oauth2AuthorizationRequestResolverRef = element.getAttribute(ATT_AUTHORIZATION_REQUEST_RESOLVER_REF); + if (!StringUtils.isEmpty(oauth2AuthorizationRequestResolverRef)) { + oauth2AuthorizationRequestRedirectFilterBuilder + .addConstructorArgReference(oauth2AuthorizationRequestResolverRef); + } else { + oauth2AuthorizationRequestRedirectFilterBuilder.addConstructorArgValue(clientRegistrationRepository); + } + + oauth2AuthorizationRequestRedirectFilterBuilder + .addPropertyValue("authorizationRequestRepository", oauth2AuthRequestRepository) + .addPropertyValue("requestCache", requestCache); + oauth2AuthorizationRequestRedirectFilter = oauth2AuthorizationRequestRedirectFilterBuilder.getBeanDefinition(); + + String authenticationSuccessHandlerRef = element.getAttribute(ATT_AUTHENTICATION_SUCCESS_HANDLER_REF); + if (!StringUtils.isEmpty(authenticationSuccessHandlerRef)) { + oauth2LoginAuthenticationFilterBuilder.addPropertyReference("authenticationSuccessHandler", + authenticationSuccessHandlerRef); + } + + String loginPage = element.getAttribute(ATT_LOGIN_PAGE); + WebConfigUtils.validateHttpRedirect(loginPage, parserContext, source); + if (!StringUtils.isEmpty(loginPage)) { + oauth2LoginAuthenticationEntryPoint = BeanDefinitionBuilder + .rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class).addConstructorArgValue(loginPage) + .addPropertyValue("portMapper", portMapper).addPropertyValue("portResolver", portResolver) + .getBeanDefinition(); + } else { + Map entryPoint = getLoginEntryPoint(element, parserContext); + + if (entryPoint != null) { + oauth2LoginAuthenticationEntryPoint = BeanDefinitionBuilder + .rootBeanDefinition(DelegatingAuthenticationEntryPoint.class).addConstructorArgValue(entryPoint) + .addPropertyValue("defaultEntryPoint", new LoginUrlAuthenticationEntryPoint(DEFAULT_LOGIN_URI)) + .getBeanDefinition(); + } + } + + String authenticationFailureHandlerRef = element.getAttribute(ATT_AUTHENTICATION_FAILURE_HANDLER_REF); + if (!StringUtils.isEmpty(authenticationFailureHandlerRef)) { + oauth2LoginAuthenticationFilterBuilder.addPropertyReference("authenticationFailureHandler", + authenticationFailureHandlerRef); + } else { + BeanDefinitionBuilder failureHandlerBuilder = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"); + failureHandlerBuilder.addConstructorArgValue( + DEFAULT_LOGIN_URI + "?" + DefaultLoginPageGeneratingFilter.ERROR_PARAMETER_NAME); + failureHandlerBuilder.addPropertyValue("allowSessionCreation", allowSessionCreation); + + oauth2LoginAuthenticationFilterBuilder.addPropertyValue("authenticationFailureHandler", + failureHandlerBuilder.getBeanDefinition()); + } + + // prepare loginlinks + oauth2LoginLinks = BeanDefinitionBuilder.rootBeanDefinition(Map.class) + .setFactoryMethodOnBean("getLoginLinks", oauth2LoginBeanConfigId).getBeanDefinition(); + + return oauth2LoginAuthenticationFilterBuilder.getBeanDefinition(); + } + + private BeanMetadataElement getOAuth2AuthorizationRequestRepository(Element element) { + BeanMetadataElement oauth2AuthRequestRepository = null; + String oauth2AuthRequestRepositoryRef = element.getAttribute(ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF); + if (!StringUtils.isEmpty(oauth2AuthRequestRepositoryRef)) { + oauth2AuthRequestRepository = new RuntimeBeanReference(oauth2AuthRequestRepositoryRef); + } else { + oauth2AuthRequestRepository = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository") + .getBeanDefinition(); + } + return oauth2AuthRequestRepository; + } + + private BeanMetadataElement getAuthorizedClientRepository(Element element, + BeanMetadataElement clientRegistrationRepository) { + BeanMetadataElement authorizedClientRepository = null; + + String authorizedClientRepositoryRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_REPOSITORY_REF); + if (!StringUtils.isEmpty(authorizedClientRepositoryRef)) { + authorizedClientRepository = new RuntimeBeanReference(authorizedClientRepositoryRef); + } else { + BeanMetadataElement oauth2AuthorizedClientService = null; + String authorizedClientServiceRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_SERVICE_REF); + if (!StringUtils.isEmpty(authorizedClientServiceRef)) { + oauth2AuthorizedClientService = new RuntimeBeanReference(authorizedClientServiceRef); + } else { + oauth2AuthorizedClientService = BeanDefinitionBuilder + .rootBeanDefinition( + "org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService") + .addConstructorArgValue(clientRegistrationRepository).getBeanDefinition(); + } + + authorizedClientRepository = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository") + .addConstructorArgValue(oauth2AuthorizedClientService).getBeanDefinition(); + } + + return authorizedClientRepository; + } + + private BeanMetadataElement getClientRegistrationRepository(Element element) { + BeanMetadataElement clientRegistrationRepository = null; + + String clientRegistrationRepositoryRef = element.getAttribute(ATT_CLIENT_REGISTRATION_REPOSITORY_REF); + if (!StringUtils.isEmpty(clientRegistrationRepositoryRef)) { + clientRegistrationRepository = new RuntimeBeanReference(clientRegistrationRepositoryRef); + } else { + clientRegistrationRepository = new RuntimeBeanReference(ClientRegistrationRepository.class); + } + return clientRegistrationRepository; + } + + private BeanDefinition getOAuth2OidcAuthProvider(Element element, BeanMetadataElement accessTokenResponseClient, + String oauth2UserAuthMapperRef, ParserContext parserContext) { + BeanDefinition oauth2OidcAuthProvider = null; + boolean oidcAuthenticationProviderEnabled = ClassUtils + .isPresent("org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader()); + + if (oidcAuthenticationProviderEnabled) { + BeanMetadataElement oidcUserService = getOAuth2OidcUserService(element); + + BeanDefinitionBuilder oauth2OidcAuthProviderBuilder = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider") + .addConstructorArgValue(accessTokenResponseClient).addConstructorArgValue(oidcUserService); + + if (!StringUtils.isEmpty(oauth2UserAuthMapperRef)) { + oauth2OidcAuthProviderBuilder.addPropertyReference("authoritiesMapper", oauth2UserAuthMapperRef); + } + + String jwtDecoderFactoryRef = element.getAttribute(ATT_JWT_DECODER_FACTORY_REF); + if (!StringUtils.isEmpty(jwtDecoderFactoryRef)) { + oauth2OidcAuthProviderBuilder.addPropertyReference("jwtDecoderFactory", jwtDecoderFactoryRef); + } + + oauth2OidcAuthProvider = oauth2OidcAuthProviderBuilder.getBeanDefinition(); + } else { + oauth2OidcAuthProvider = BeanDefinitionBuilder.rootBeanDefinition(OidcAuthenticationRequestChecker.class) + .getBeanDefinition(); + } + + return oauth2OidcAuthProvider; + } + + private BeanMetadataElement getOAuth2OidcUserService(Element element) { + BeanMetadataElement oauth2OidcUserService = null; + String oauth2UserServiceRef = element.getAttribute(ATT_OIDC_USER_SERVICE_REF); + if (!StringUtils.isEmpty(oauth2UserServiceRef)) { + oauth2OidcUserService = new RuntimeBeanReference(oauth2UserServiceRef); + } else { + oauth2OidcUserService = BeanDefinitionBuilder + .rootBeanDefinition("org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService") + .getBeanDefinition(); + } + return oauth2OidcUserService; + } + + private BeanMetadataElement getOAuth2UserService(Element element) { + BeanMetadataElement oauth2UserService = null; + String oauth2UserServiceRef = element.getAttribute(ATT_USER_SERVICE_REF); + if (!StringUtils.isEmpty(oauth2UserServiceRef)) { + oauth2UserService = new RuntimeBeanReference(oauth2UserServiceRef); + } else { + oauth2UserService = BeanDefinitionBuilder + .rootBeanDefinition("org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService") + .getBeanDefinition(); + } + return oauth2UserService; + } + + private BeanMetadataElement getAccessTokenResponseClient(Element element) { + BeanMetadataElement accessTokenResponseClient = null; + + String accessTokenResponseClientRef = element.getAttribute(ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF); + if (!StringUtils.isEmpty(accessTokenResponseClientRef)) { + accessTokenResponseClient = new RuntimeBeanReference(accessTokenResponseClientRef); + } else { + accessTokenResponseClient = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient") + .getBeanDefinition(); + } + return accessTokenResponseClient; + } + + BeanDefinition getOAuth2AuthorizationRequestRedirectFilter() { + return oauth2AuthorizationRequestRedirectFilter; + } + + BeanDefinition getOAuth2LoginAuthenticationEntryPoint() { + return oauth2LoginAuthenticationEntryPoint; + } + + BeanDefinition getOAuth2LoginAuthenticationProvider() { + return oauth2LoginAuthenticationProvider; + } + + BeanDefinition getOAuth2LoginOidcAuthenticationProvider() { + return oauth2LoginOidcAuthenticationProvider; + } + + BeanDefinition getOAuth2LoginLinks() { + return oauth2LoginLinks; + } + + private Map getLoginEntryPoint(Element element, + ParserContext parserContext) { + Map entryPoints = null; + Element clientRegsElt = DomUtils.getChildElementByTagName(element.getOwnerDocument().getDocumentElement(), + Elements.CLIENT_REGISTRATIONS); + + if (clientRegsElt != null) { + List clientRegList = DomUtils.getChildElementsByTagName(clientRegsElt, ELT_CLIENT_REGISTRATION); + + if (clientRegList.size() == 1) { + RequestMatcher loginPageMatcher = new AntPathRequestMatcher(DEFAULT_LOGIN_URI); + RequestMatcher faviconMatcher = new AntPathRequestMatcher("/favicon.ico"); + RequestMatcher defaultEntryPointMatcher = this.getAuthenticationEntryPointMatcher(); + RequestMatcher defaultLoginPageMatcher = new AndRequestMatcher( + new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); + + RequestMatcher notXRequestedWith = new NegatedRequestMatcher( + new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); + + Element clientRegElt = clientRegList.get(0); + entryPoints = new LinkedHashMap<>(); + entryPoints.put( + new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher)), + new LoginUrlAuthenticationEntryPoint(DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + + clientRegElt.getAttribute(ATT_REGISTRATION_ID))); + + } + } + + return entryPoints; + } + + private RequestMatcher getAuthenticationEntryPointMatcher() { + ContentNegotiationStrategy contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); + + MediaTypeRequestMatcher mediaMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, + MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML, + MediaType.TEXT_PLAIN); + mediaMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + + RequestMatcher notXRequestedWith = new NegatedRequestMatcher( + new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); + + return new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher)); + } + + private static class OidcAuthenticationRequestChecker implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication; + + // Section 3.1.2.1 Authentication Request - + // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // scope + // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. + if (authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getScopes() + .contains(OidcScopes.OPENID)) { + + OAuth2Error oauth2Error = new OAuth2Error("oidc_provider_not_configured", + "An OpenID Connect Authentication Provider has not been configured. " + + "Check to ensure you include the dependency 'spring-security-oauth2-jose'.", + null); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + return null; + } + + @Override + public boolean supports(Class authentication) { + return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication); + } + } + + /** + * Wrapper bean class to provide configuration from applicationContext + */ + private static class OAuth2LoginBeanConfig implements ApplicationContextAware { + + private ApplicationContext appContext; + + @Override + public void setApplicationContext(ApplicationContext appContext) throws BeansException { + this.appContext = appContext; + } + + @SuppressWarnings({ "unchecked", "unused" }) + public Map getLoginLinks() { + Iterable clientRegistrations = null; + ClientRegistrationRepository clientRegistrationRepository = appContext + .getBean(ClientRegistrationRepository.class); + ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class); + if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) { + clientRegistrations = (Iterable) clientRegistrationRepository; + } + if (clientRegistrations == null) { + return Collections.emptyMap(); + } + + String authorizationRequestBaseUri = DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + Map loginUrlToClientName = new HashMap<>(); + clientRegistrations.forEach(registration -> loginUrlToClientName.put( + authorizationRequestBaseUri + "/" + registration.getRegistrationId(), + registration.getClientName())); + + return loginUrlToClientName; + } + } } diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index 3c8d1561cc..6a759fc3dc 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2020 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. @@ -34,9 +34,11 @@ enum SecurityFilters { HEADERS_FILTER, CORS_FILTER, CSRF_FILTER, LOGOUT_FILTER, + OAUTH2_REDIRECT_FILTER, X509_FILTER, PRE_AUTH_FILTER, CAS_FILTER, + OAUTH2_LOGIN_FILTER, FORM_LOGIN_FILTER, OPENID_FILTER, LOGIN_PAGE_FILTER, diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java index 368538e25b..650a82886c 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -15,19 +15,220 @@ */ package org.springframework.security.config.oauth2.client; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrations; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthenticationMethod; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; /** - * @author + * @author Ruby Hartono + * @since 5.3 */ public final class ClientRegistrationsBeanDefinitionParser implements BeanDefinitionParser { + private static final String ELT_CLIENT_REGISTRATION = "client-registration"; + private static final String ELT_PROVIDER = "provider"; + private static final String ATT_REGISTRATION_ID = "registration-id"; + private static final String ATT_CLIENT_ID = "client-id"; + private static final String ATT_CLIENT_SECRET = "client-secret"; + private static final String ATT_CLIENT_AUTHENTICATION_METHOD = "client-authentication-method"; + private static final String ATT_AUTHORIZATION_GRANT_TYPE = "authorization-grant-type"; + private static final String ATT_REDIRECT_URI = "redirect-uri"; + private static final String ATT_SCOPE = "scope"; + private static final String ATT_CLIENT_NAME = "client-name"; + private static final String ATT_PROVIDER_ID = "provider-id"; + private static final String ATT_AUTHORIZATION_URI = "authorization-uri"; + private static final String ATT_TOKEN_URI = "token-uri"; + private static final String ATT_USERINFO_URI = "userinfo-uri"; + private static final String ATT_USERINFO_AUTHENTICATION_METHOD = "userinfo-authentication-method"; + private static final String ATT_USERNAME_ATTRIBUTE_NAME = "username-attribute-name"; + private static final String ATT_JWKSET_URI = "jwkset-uri"; + private static final String ATT_ISSUER_URI = "issuer-uri"; + @Override public BeanDefinition parse(Element element, ParserContext parserContext) { - // TODO Implement + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), + parserContext.extractSource(element)); + parserContext.pushContainingComponent(compositeDef); + + Map> providerDetailMap = getProviders(element); + + List clientRegs = getClientRegistrations(element, parserContext, providerDetailMap); + + BeanDefinition inMemClientRegRepoBeanDef = BeanDefinitionBuilder + .rootBeanDefinition(InMemoryClientRegistrationRepository.class).addConstructorArgValue(clientRegs) + .getBeanDefinition(); + String beanName = parserContext.getReaderContext().generateBeanName(inMemClientRegRepoBeanDef); + parserContext.registerBeanComponent(new BeanComponentDefinition(inMemClientRegRepoBeanDef, beanName)); + + parserContext.popAndRegisterContainingComponent(); return null; } + + private List getClientRegistrations(Element element, ParserContext parserContext, + Map> providerDetailMap) { + List clientRegElts = DomUtils.getChildElementsByTagName(element, ELT_CLIENT_REGISTRATION); + List clientRegs = new ArrayList<>(); + + for (Element clientRegElt : clientRegElts) { + String regId = clientRegElt.getAttribute(ATT_REGISTRATION_ID); + String clientId = clientRegElt.getAttribute(ATT_CLIENT_ID); + String clientSecret = clientRegElt.getAttribute(ATT_CLIENT_SECRET); + String clientAuthMethod = clientRegElt.getAttribute(ATT_CLIENT_AUTHENTICATION_METHOD); + String authGrantType = clientRegElt.getAttribute(ATT_AUTHORIZATION_GRANT_TYPE); + String redirectUri = clientRegElt.getAttribute(ATT_REDIRECT_URI); + String scope = clientRegElt.getAttribute(ATT_SCOPE); + String clientName = clientRegElt.getAttribute(ATT_CLIENT_NAME); + String providerId = clientRegElt.getAttribute(ATT_PROVIDER_ID); + + Set scopes = StringUtils.commaDelimitedListToSet(scope); + ClientRegistration.Builder builder = getBuilderFromIssuerIfPossible(regId, providerId, providerDetailMap); + if (builder == null) { + builder = getBuilder(regId, providerId, providerDetailMap); + if (builder == null) { + Object source = parserContext.extractSource(element); + parserContext.getReaderContext().error(getErrorMessage(providerId, regId), source); + // error on the config skip to next element + break; + } + } + + ClientRegistration clientReg = builder.clientId(clientId) + .clientSecret(clientSecret) + .clientAuthenticationMethod(new ClientAuthenticationMethod(clientAuthMethod)) + .authorizationGrantType(new AuthorizationGrantType(authGrantType)) + .redirectUriTemplate(redirectUri) + .scope(scopes) + .clientName(clientName) + .build(); + clientRegs.add(clientReg); + } + return clientRegs; + } + + private Map> getProviders(Element element) { + List providerRegElts = DomUtils.getChildElementsByTagName(element, ELT_PROVIDER); + Map> providerDetailMap = new HashMap<>(); + for (Element providerRegElt : providerRegElts) { + Map detail = new HashMap(); + String providerId = providerRegElt.getAttribute(ATT_PROVIDER_ID); + detail.put(ATT_PROVIDER_ID, providerId); + detail.put(ATT_AUTHORIZATION_URI, providerRegElt.getAttribute(ATT_AUTHORIZATION_URI)); + detail.put(ATT_TOKEN_URI, providerRegElt.getAttribute(ATT_TOKEN_URI)); + detail.put(ATT_USERINFO_URI, providerRegElt.getAttribute(ATT_USERINFO_URI)); + detail.put(ATT_USERINFO_AUTHENTICATION_METHOD, + providerRegElt.getAttribute(ATT_USERINFO_AUTHENTICATION_METHOD)); + detail.put(ATT_USERNAME_ATTRIBUTE_NAME, providerRegElt.getAttribute(ATT_USERNAME_ATTRIBUTE_NAME)); + detail.put(ATT_JWKSET_URI, providerRegElt.getAttribute(ATT_JWKSET_URI)); + detail.put(ATT_ISSUER_URI, providerRegElt.getAttribute(ATT_ISSUER_URI)); + + providerDetailMap.put(providerId, detail); + } + return providerDetailMap; + } + + private static ClientRegistration.Builder getBuilderFromIssuerIfPossible(String registrationId, + String configuredProviderId, Map> providers) { + String providerId = (configuredProviderId != null) ? configuredProviderId : registrationId; + if (providers.containsKey(providerId)) { + Map provider = providers.get(providerId); + String issuer = provider.get(ATT_ISSUER_URI); + if (!StringUtils.isEmpty(issuer)) { + ClientRegistration.Builder builder = ClientRegistrations.fromIssuerLocation(issuer) + .registrationId(registrationId); + return getBuilder(builder, provider); + } + } + return null; + } + + private static ClientRegistration.Builder getBuilder(String registrationId, String configuredProviderId, + Map> providers) { + String providerId = (configuredProviderId != null) ? configuredProviderId : registrationId; + CommonOAuth2Provider provider = getCommonProvider(providerId); + if (provider == null && !providers.containsKey(providerId)) { + return null; + } + ClientRegistration.Builder builder = (provider != null) ? provider.getBuilder(registrationId) + : ClientRegistration.withRegistrationId(registrationId); + if (providers.containsKey(providerId)) { + return getBuilder(builder, providers.get(providerId)); + } + return builder; + } + + private static ClientRegistration.Builder getBuilder(ClientRegistration.Builder builder, + Map provider) { + getOptionalIfNotEmpty(provider.get(ATT_AUTHORIZATION_URI)).ifPresent(builder::authorizationUri); + getOptionalIfNotEmpty(provider.get(ATT_TOKEN_URI)).ifPresent(builder::tokenUri); + getOptionalIfNotEmpty(provider.get(ATT_USERINFO_URI)).ifPresent(builder::userInfoUri); + getOptionalIfNotEmpty(provider.get(ATT_USERINFO_AUTHENTICATION_METHOD)).map(AuthenticationMethod::new) + .ifPresent(builder::userInfoAuthenticationMethod); + getOptionalIfNotEmpty(provider.get(ATT_JWKSET_URI)).ifPresent(builder::jwkSetUri); + getOptionalIfNotEmpty(provider.get(ATT_USERNAME_ATTRIBUTE_NAME)).ifPresent(builder::userNameAttributeName); + return builder; + } + + private static Optional getOptionalIfNotEmpty(String str) { + return Optional.ofNullable(str).filter(s -> !s.isEmpty()); + } + + private static CommonOAuth2Provider getCommonProvider(String providerId) { + try { + String value = providerId.toString().trim(); + if (value.isEmpty()) { + return null; + } + try { + return CommonOAuth2Provider.valueOf(value); + } catch (Exception ex) { + return findEnum(value); + } + } catch (Exception ex) { + return null; + } + } + + private static CommonOAuth2Provider findEnum(String value) { + String name = getCanonicalName(value); + for (CommonOAuth2Provider candidate : EnumSet.allOf(CommonOAuth2Provider.class)) { + String candidateName = getCanonicalName(candidate.name()); + if (name.equals(candidateName)) { + return candidate; + } + } + throw new IllegalArgumentException( + "No enum constant " + CommonOAuth2Provider.class.getCanonicalName() + "." + value); + } + + private static String getCanonicalName(String name) { + StringBuilder canonicalName = new StringBuilder(name.length()); + name.chars().filter(Character::isLetterOrDigit).map(Character::toLowerCase) + .forEach((c) -> canonicalName.append((char) c)); + return canonicalName.toString(); + } + + private static String getErrorMessage(String configuredProviderId, String registrationId) { + return ((configuredProviderId != null) ? "Unknown provider ID '" + configuredProviderId + "'" + : "Provider ID must be specified for client registration '" + registrationId + "'"); + } } diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.3.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.3.rnc index d2dfd25957..92afc6a5c5 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.3.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.3.rnc @@ -439,11 +439,53 @@ form-login.attlist &= oauth2-login = ## Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. - element oauth2-login {empty} + element oauth2-login {oauth2-login.attlist} +oauth2-login.attlist &= + ## Reference to ClientRegistrationRepository + attribute client-registration-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to OAuth2AuthorizedClientRepository + attribute authorized-client-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to OAuth2AuthorizedClientService + attribute authorized-client-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to AuthorizationRequestRepository + attribute authorization-request-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to OAuth2AuthorizationRequestResolver + attribute authorization-request-resolver-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to OAuth2AccessTokenResponseClient + attribute access-token-response-client-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to GrantedAuthoritiesMapper + attribute user-authorities-mapper-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to OAuth2UserService + attribute user-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to OidcUserService + attribute oidc-user-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Specifies the URL to validate the credentials. + attribute login-processing-url {xsd:token}? +oauth2-login.attlist &= + ## Specifies the URL to send users to if login is required + attribute login-page {xsd:token}? +oauth2-login.attlist &= + ## Specifies authentication success handler + attribute authentication-success-handler-ref {xsd:token}? +oauth2-login.attlist &= + ## Specifies authentication failure handler + attribute authentication-failure-handler-ref {xsd:token}? +oauth2-login.attlist &= + ## Specifies JWT decoder factory for OidcAuthorizationCodeAuthenticationProvider + attribute jwt-decoder-factory-ref {xsd:token}? client-registrations = ## Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - element client-registrations {client-registration+, provider?} + element client-registrations {client-registration+, provider*} client-registration = ## Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. @@ -487,7 +529,7 @@ provider.attlist &= attribute authorization-uri {xsd:token}? provider.attlist &= ## The Token Endpoint URI for the Authorization Server. - attribute token-uri {xsd:token} + attribute token-uri {xsd:token}? provider.attlist &= ## The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. attribute userinfo-uri {xsd:token}? @@ -982,4 +1024,4 @@ position = ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. attribute position {named-security-filter} -named-security-filter = "FIRST" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "CSRF_FILTER" | "LOGOUT_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" +named-security-filter = "FIRST" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "CSRF_FILTER" | "LOGOUT_FILTER" | "OAUTH2_REDIRECT_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.3.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.3.xsd index b776d3f0e6..7a4d9aa4fa 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.3.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.3.xsd @@ -1456,8 +1456,96 @@ Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. - + + + + + + + Reference to ClientRegistrationRepository + + + + + + Reference to OAuth2AuthorizedClientRepository + + + + + + Reference to OAuth2AuthorizedClientService + + + + + + Reference to AuthorizationRequestRepository + + + + + + Reference to OAuth2AuthorizationRequestResolver + + + + + + Reference to OAuth2AccessTokenResponseClient + + + + + + Reference to GrantedAuthoritiesMapper + + + + + + Reference to OAuth2UserService + + + + + + Reference to OidcUserService + + + + + + Specifies the URL to validate the credentials. + + + + + + Specifies the URL to send users to if login is required + + + + + + Specifies authentication success handler + + + + + + Specifies authentication failure handler + + + + + + Specifies JWT decoder factory for OidcAuthorizationCodeAuthenticationProvider + + + + Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 @@ -1467,7 +1555,7 @@ - + @@ -1580,7 +1668,7 @@ - + The Token Endpoint URI for the Authorization Server. @@ -2901,9 +2989,11 @@ + + diff --git a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java index 6589db8047..05bb917733 100644 --- a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java +++ b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java index 555b2bb72e..d8a1b5e1b6 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -17,39 +17,622 @@ package org.springframework.security.config.http; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.internal.util.MockUtil; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.core.user.TestOAuth2Users; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.oauth2.jwt.TestJwts; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses.accessTokenResponse; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + /** * Tests for {@link OAuth2LoginBeanDefinitionParser}. * - * @author + * @author Ruby Hartono */ public class OAuth2LoginBeanDefinitionParserTests { - private static final String CONFIG_LOCATION_PREFIX = - "classpath:org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests"; + private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests"; @Rule public final SpringTestRule spring = new SpringTestRule(); + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + @Autowired + private OAuth2LoginAuthenticationFilter oauth2LoginAuthenticationFilter; + + @Autowired(required = false) + private OAuth2AuthorizedClientRepository oauth2AuthorizedClientRepository; + + @Autowired(required = false) + private OAuth2AuthorizedClientService oauth2AuthorizedClientService; + + @Autowired + SecurityContextRepository securityContextRepository; + + @Autowired(required = false) + private ApplicationListener authenticationSuccessListener; + + @Autowired(required = false) + private AuthorizationRequestRepository authorizationRequestRepository; + + @Autowired(required = false) + private OAuth2AccessTokenResponseClient accessTokenResponseClient; + + @Autowired(required = false) + private OAuth2UserService oauth2UserService; + + @Autowired(required = false) + private JwtDecoderFactory jwtDecoderFactory; + + @Autowired(required = false) + private OAuth2AuthorizationRequestResolver oauth2AuthorizationRequestResolver; + + @Autowired(required = false) + private GrantedAuthoritiesMapper grantedAuthoritiesMapper; + + @Autowired(required = false) + private AuthenticationFailureHandler authenticationFailureHandler; + + @Autowired(required = false) + private AuthenticationSuccessHandler authenticationSuccessHandler; + @Autowired private MockMvc mvc; + @Test + public void requestLoginWhenMultiClientRegistrationThenReturnLoginPageWithOauth2Login() throws Exception { + this.spring.configLocations(this.xml("MultiClientRegistration")).autowire(); + + MvcResult result = this.mvc.perform(get("/login")).andExpect(status().is2xxSuccessful()).andReturn(); + + assertThat(result.getResponse().getContentAsString()) + .contains("Google"); + assertThat(result.getResponse().getContentAsString()) + .contains("Github"); + } + + // gh-5347 @Test public void requestWhenSingleClientRegistrationThenAutoRedirect() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithinSameFile")).autowire(); + + this.mvc.perform(get("/")).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/oauth2/authorization/google-login")); + } + + // gh-5347 + @Test + public void requestWhenSingleClientRegistrationAndRequestFaviconNotAuthenticatedThenRedirectDefaultLoginPage() + throws Exception { this.spring.configLocations(this.xml("SingleClientRegistration")).autowire(); - this.mvc.perform(get("/")) - .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrlPattern("/oauth2/authorization/google-login")); + this.mvc.perform(get("/favicon.ico").accept(new MediaType("image", "*"))).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + // gh-6812 + @Test + public void requestWhenSingleClientRegistrationAndRequestXHRNotAuthenticatedThenDoesNotRedirectForAuthorization() + throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration")).autowire(); + + this.mvc.perform(get("/").header("X-Requested-With", "XMLHttpRequest")).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + public void requestWhenAuthorizationRequestNotFoundThenThrowAuthenticationException() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomAuthenticationFailureHandler")) + .autowire(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", "state123"); + this.mvc.perform(get("/login/oauth2/code/google").params(params)); + + // assertions + ArgumentCaptor exceptionCaptor = ArgumentCaptor + .forClass(AuthenticationException.class); + verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), exceptionCaptor.capture()); + AuthenticationException excValue = exceptionCaptor.getValue(); + assertThat(excValue).isInstanceOf(OAuth2AuthenticationException.class); + assertThat(((OAuth2AuthenticationException) excValue).getError().getErrorCode()) + .isEqualTo("authorization_request_not_found"); + } + + @Test + public void requestWhenAuthorizationResponseValidThenAuthenticate() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithTestConfiguration")).autowire(); + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User oauth2User = TestOAuth2Users.create(); + when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User); + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login"); + OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes) + .build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authRequest.getState()); + this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is2xxSuccessful()); + + ArgumentCaptor authenticationCaptor = ArgumentCaptor.forClass(Authentication.class); + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture()); + Authentication authenticationValue = authenticationCaptor.getValue(); + assertThat(authenticationValue.getAuthorities()).hasSize(1); + assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class) + .hasToString("ROLE_USER"); + } + + // gh-6009 + @Test + public void requestWhenAuthorizationResponseValidThenAuthenticationSuccessEventPublished() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithTestConfiguration")).autowire(); + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User oauth2User = TestOAuth2Users.create(); + when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User); + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login"); + OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes) + .build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authRequest.getState()); + this.mvc.perform(get("/login/oauth2/code/google").params(params)); + + // assertions + verify(authenticationSuccessListener).onApplicationEvent(any(AuthenticationSuccessEvent.class)); + } + + @Test + public void requestWhenOidcAuthenticationResponseValidThenJwtDecoderFactoryCalled() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler")) + .autowire(); + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OidcParameterNames.ID_TOKEN, "token123"); + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters) + .build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + Jwt jwt = TestJwts.user(); + when(this.jwtDecoderFactory.createDecoder(any())).thenReturn(token -> jwt); + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login"); + OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes) + .scope("openid").build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authRequest.getState()); + this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void requestWhenCustomGrantedAuthoritiesMapperThenCalled() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomGrantedAuthorities")).autowire(); + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OidcParameterNames.ID_TOKEN, "token123"); + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters) + .build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User oauth2User = TestOAuth2Users.create(); + when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User); + + GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_OAUTH2_USER"); + when(this.grantedAuthoritiesMapper.mapAuthorities(any())) + .thenReturn((Collection) Collections.singletonList(grantedAuthority)); + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login"); + OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes) + .build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authRequest.getState()); + this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is2xxSuccessful()); + + ArgumentCaptor authenticationCaptor = ArgumentCaptor.forClass(Authentication.class); + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture()); + Authentication authenticationValue = authenticationCaptor.getValue(); + assertThat(authenticationValue.getAuthorities()).hasSize(1); + assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class) + .hasToString("ROLE_OAUTH2_USER"); + + // re-setup for OIDC test + Jwt jwt = TestJwts.user(); + when(this.jwtDecoderFactory.createDecoder(any())).thenReturn(token -> jwt); + + grantedAuthority = new SimpleGrantedAuthority("ROLE_OIDC_USER"); + when(this.grantedAuthoritiesMapper.mapAuthorities(any())) + .thenReturn((Collection) Collections.singletonList(grantedAuthority)); + + authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes).scope("openid").build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is2xxSuccessful()); + + authenticationCaptor = ArgumentCaptor.forClass(Authentication.class); + verify(authenticationSuccessHandler, times(2)).onAuthenticationSuccess(any(), any(), + authenticationCaptor.capture()); + authenticationValue = authenticationCaptor.getValue(); + assertThat(authenticationValue.getAuthorities()).hasSize(1); + assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class) + .hasToString("ROLE_OIDC_USER"); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void successOidcLoginWhenSingleClientRegistrationAndCustomAuthoritiesThenReturnSuccessWithCorrectAuthorities() + throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithJwtDecoderFactory")).autowire(); + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OidcParameterNames.ID_TOKEN, "token123"); + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters) + .build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + Jwt jwt = TestJwts.user(); + when(this.jwtDecoderFactory.createDecoder(any())).thenReturn(token -> jwt); + + GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_OIDC_USER"); + when(this.grantedAuthoritiesMapper.mapAuthorities(any())) + .thenReturn((Collection) Collections.singletonList(grantedAuthority)); + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login"); + OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes) + .scope("openid").build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authRequest.getState()); + this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is2xxSuccessful()); + + ArgumentCaptor authenticationCaptor = ArgumentCaptor.forClass(Authentication.class); + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture()); + Authentication authenticationValue = authenticationCaptor.getValue(); + assertThat(authenticationValue.getAuthorities()).hasSize(1); + assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class) + .hasToString("ROLE_OIDC_USER"); + } + + // gh-5488 + @Test + public void requestWhenCustomLoginProcessingUrlThenProcessAuthentication() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomLoginProcessingUrl")).autowire(); + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User oauth2User = TestOAuth2Users.create(); + when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User); + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login"); + OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes) + .build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authRequest.getState()); + this.mvc.perform(get("/login/oauth2/google").params(params)).andExpect(status().is2xxSuccessful()); + + ArgumentCaptor authenticationCaptor = ArgumentCaptor.forClass(Authentication.class); + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture()); + Authentication authenticationValue = authenticationCaptor.getValue(); + assertThat(authenticationValue.getAuthorities()).hasSize(1); + assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class) + .hasToString("ROLE_USER"); + } + + // gh-5521 + @Test + public void requestWhenCustomAuthorizationRequestResolverThenCalled() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomAuthorizationRequestResolver")) + .autowire(); + + this.mvc.perform(get("/oauth2/authorization/google")).andExpect(status().is3xxRedirection()); + + verify(oauth2AuthorizationRequestResolver).resolve(any()); + } + + // gh-5347 + @Test + public void requestWhenMultipleClientsConfiguredThenRedirectDefaultLoginPage() throws Exception { + this.spring.configLocations(this.xml("MultiClientRegistration")).autowire(); + + this.mvc.perform(get("/")).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + public void requestWhenCustomLoginPageThenRedirectCustomLoginPage() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomLoginPage")).autowire(); + + this.mvc.perform(get("/")).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/custom-login")); + } + + // gh-6802 + @Test + public void requestWhenSingleClientRegistrationAndFormLoginConfiguredThenRedirectDefaultLoginPage() + throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithFormLogin")).autowire(); + + this.mvc.perform(get("/")).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + public void requestWhenCustomClientRegistrationRepositoryThenCalled() throws Exception { + this.spring.configLocations(this.xml("WithCustomClientRegistrationRepository")).autowire(); + + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User oauth2User = TestOAuth2Users.create(); + when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User); + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login"); + OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes) + .build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authRequest.getState()); + this.mvc.perform(get("/login/oauth2/code/google").params(params)); + + assertThat(MockUtil.isMock(clientRegistrationRepository)).isTrue(); + verify(clientRegistrationRepository).findByRegistrationId("google-login"); + + Field field = oauth2LoginAuthenticationFilter.getClass().getDeclaredField("clientRegistrationRepository"); + field.setAccessible(true); + Object fieldVal = field.get(oauth2LoginAuthenticationFilter); + assertThat(MockUtil.isMock(fieldVal)).isTrue(); + assertThat(fieldVal).isSameAs(clientRegistrationRepository); + } + + @Test + public void requestWhenCustomAuthorizedClientRepositoryThenCalled() throws Exception { + this.spring.configLocations(this.xml("WithCustomAuthorizedClientRepository")).autowire(); + + ClientRegistration clientReg = TestClientRegistrations.clientRegistration().build(); + when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(clientReg); + + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User oauth2User = TestOAuth2Users.create(); + when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User); + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login"); + OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes) + .build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authRequest.getState()); + this.mvc.perform(get("/login/oauth2/code/registration-id").params(params)); + + assertThat(MockUtil.isMock(oauth2AuthorizedClientRepository)).isTrue(); + verify(oauth2AuthorizedClientRepository).saveAuthorizedClient(any(), any(), any(), any()); + + Field field = oauth2LoginAuthenticationFilter.getClass().getDeclaredField("authorizedClientRepository"); + field.setAccessible(true); + Object fieldVal = field.get(oauth2LoginAuthenticationFilter); + assertThat(MockUtil.isMock(fieldVal)).isTrue(); + assertThat(fieldVal).isSameAs(oauth2AuthorizedClientRepository); + } + + @Test + public void requestWhenCustomAuthorizedClientServiceThenCalled() throws Exception { + this.spring.configLocations(this.xml("WithCustomAuthorizedClientService")).autowire(); + + ClientRegistration clientReg = TestClientRegistrations.clientRegistration().build(); + when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(clientReg); + + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User oauth2User = TestOAuth2Users.create(); + when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User); + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login"); + OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes) + .build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authRequest.getState()); + this.mvc.perform(get("/login/oauth2/code/registration-id").params(params)); + + assertThat(MockUtil.isMock(oauth2AuthorizedClientService)).isTrue(); + verify(oauth2AuthorizedClientService).saveAuthorizedClient(any(), any()); + + Field authorizedClientRepositoryField = oauth2LoginAuthenticationFilter.getClass() + .getDeclaredField("authorizedClientRepository"); + authorizedClientRepositoryField.setAccessible(true); + Object authorizedClientRepositoryFieldVal = authorizedClientRepositoryField + .get(oauth2LoginAuthenticationFilter); + assertThat(authorizedClientRepositoryFieldVal) + .isInstanceOf(AuthenticatedPrincipalOAuth2AuthorizedClientRepository.class); + + Field authorizedClientServiceField = authorizedClientRepositoryFieldVal.getClass() + .getDeclaredField("authorizedClientService"); + authorizedClientServiceField.setAccessible(true); + Object authorizedClientServiceFieldVal = authorizedClientServiceField.get(authorizedClientRepositoryFieldVal); + assertThat(MockUtil.isMock(authorizedClientServiceFieldVal)).isTrue(); + assertThat(authorizedClientServiceFieldVal).isSameAs(oauth2AuthorizedClientService); + } + + @Test + public void requestWhenCustomAuthorizationRequestRepositoryThenCalled() throws Exception { + this.spring.configLocations(this.xml("WithCustomAuthorizationRequestRepository")).autowire(); + + ClientRegistration clientReg = TestClientRegistrations.clientRegistration().build(); + when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(clientReg); + + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User oauth2User = TestOAuth2Users.create(); + when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", "state"); + this.mvc.perform(get("/login/oauth2/code/registration-id").params(params)); + + assertThat(MockUtil.isMock(authorizationRequestRepository)).isTrue(); + verify(authorizationRequestRepository).removeAuthorizationRequest(any(), any()); + + Field authorizationRequestRepositoryField = oauth2LoginAuthenticationFilter.getClass() + .getDeclaredField("authorizationRequestRepository"); + authorizationRequestRepositoryField.setAccessible(true); + Object authorizationRequestRepositoryFieldVal = authorizationRequestRepositoryField + .get(oauth2LoginAuthenticationFilter); + assertThat(MockUtil.isMock(authorizationRequestRepositoryFieldVal)).isTrue(); + assertThat(authorizationRequestRepositoryFieldVal).isSameAs(authorizationRequestRepository); + } + + @Test + public void requestWhenCustomAuthenticationSuccessHandlerThenCalled() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomAuthenticationHandler")).autowire(); + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User oauth2User = TestOAuth2Users.create(); + when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User); + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login"); + OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes) + .build(); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authRequest.getState()); + this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is2xxSuccessful()); + + ArgumentCaptor authenticationCaptor = ArgumentCaptor.forClass(Authentication.class); + verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture()); + Authentication authenticationValue = authenticationCaptor.getValue(); + assertThat(authenticationValue.getAuthorities()).hasSize(1); + assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class) + .hasToString("ROLE_USER"); + + } + + @Test + public void requestWhenCustomAuthenticationFailureHandlerThenCalled() throws Exception { + this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomAuthenticationHandler")).autowire(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", "state123"); + this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().isIAmATeapot()); } private String xml(String configName) { return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; } + + public static class TeapotAuthenticationHandler + implements AuthenticationSuccessHandler, AuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) { + response.setStatus(HttpStatus.I_AM_A_TEAPOT.value()); + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) { + response.setStatus(HttpStatus.I_AM_A_TEAPOT.value()); + } + } + } diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java new file mode 100644 index 0000000000..f6a40c9dbc --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2020 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.oauth2.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthenticationMethod; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + +/** + * Tests for {@link ClientRegistrationsBeanDefinitionParser}. + * + * @author Ruby Hartono + */ +public class ClientRegistrationsBeanDefinitionParserTests { + private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + private static final String DEFAULT_RESPONSE = + "{\n" + + " \"authorization_endpoint\": \"https://example.com/o/oauth2/v2/auth\", \n" + + " \"claims_supported\": [\n" + + " \"aud\", \n" + + " \"email\", \n" + + " \"email_verified\", \n" + + " \"exp\", \n" + + " \"family_name\", \n" + + " \"given_name\", \n" + + " \"iat\", \n" + + " \"iss\", \n" + + " \"locale\", \n" + + " \"name\", \n" + + " \"picture\", \n" + + " \"sub\"\n" + + " ], \n" + + " \"code_challenge_methods_supported\": [\n" + + " \"plain\", \n" + + " \"S256\"\n" + + " ], \n" + + " \"id_token_signing_alg_values_supported\": [\n" + + " \"RS256\"\n" + + " ], \n" + + " \"issuer\": \"http://localhost:49259\", \n" + + " \"jwks_uri\": \"https://example.com/oauth2/v3/certs\", \n" + + " \"response_types_supported\": [\n" + + " \"code\", \n" + + " \"token\", \n" + + " \"id_token\", \n" + + " \"code token\", \n" + + " \"code id_token\", \n" + + " \"token id_token\", \n" + + " \"code token id_token\", \n" + + " \"none\"\n" + + " ], \n" + + " \"revocation_endpoint\": \"https://example.com/o/oauth2/revoke\", \n" + + " \"scopes_supported\": [\n" + + " \"openid\", \n" + + " \"email\", \n" + + " \"profile\"\n" + + " ], \n" + + " \"subject_types_supported\": [\n" + + " \"public\"\n" + + " ], \n" + + " \"grant_types_supported\" : [\"authorization_code\"], \n" + + " \"token_endpoint\": \"https://example.com/oauth2/v4/token\", \n" + + " \"token_endpoint_auth_methods_supported\": [\n" + + " \"client_secret_post\", \n" + + " \"client_secret_basic\", \n" + + " \"none\"\n" + + " ], \n" + + " \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n" + + "}"; + + @Test + public void parseWhenIssuerUriConfiguredThenRequestConfigFromIssuer() throws Exception { + MockWebServer server = new MockWebServer(); + ObjectMapper mapper = new ObjectMapper(); + server.start(49259); + Map response = mapper.readValue(DEFAULT_RESPONSE, new TypeReference>() { + }); + final String responseBody = mapper.writeValueAsString(response); + + final Dispatcher oidcDispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/issuer1/.well-known/openid-configuration": + case "/.well-known/openid-configuration": + return new MockResponse().setResponseCode(200).setBody(responseBody) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + } + return new MockResponse().setResponseCode(404); + } + }; + + final Dispatcher oauthDispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/.well-known/oauth-authorization-server/issuer1": + case "/.well-known/oauth-authorization-server": + return new MockResponse().setResponseCode(200).setBody(responseBody) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + } + return new MockResponse().setResponseCode(404); + } + }; + + server.setDispatcher(oidcDispatcher); + + this.spring.configLocations(this.xml("FromIssuerUri")).autowire(); + + assertThat(clientRegistrationRepository).isInstanceOf(InMemoryClientRegistrationRepository.class); + + testIssuerUriResponse(clientRegistrationRepository); + + // test oauth + server.setDispatcher(oauthDispatcher); + this.spring.configLocations(this.xml("FromIssuerUri")).autowire(); + testIssuerUriResponse(clientRegistrationRepository); + + server.shutdown(); + server.close(); + } + + private void testIssuerUriResponse(ClientRegistrationRepository clientRegistrationRepository) { + ClientRegistration googleLogin = clientRegistrationRepository.findByRegistrationId("google-login"); + assertThat(googleLogin).isNotNull(); + assertThat(googleLogin.getRegistrationId()).isEqualTo("google-login"); + assertThat(googleLogin.getClientId()).isEqualTo("google-client-id"); + assertThat(googleLogin.getClientSecret()).isEqualTo("google-client-secret"); + assertThat(googleLogin.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + assertThat(googleLogin.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(googleLogin.getRedirectUriTemplate()).isEqualTo("{baseUrl}/login/oauth2/code/{registrationId}"); + assertThat(googleLogin.getScopes()).isEqualTo(StringUtils.commaDelimitedListToSet("openid,profile,email")); + assertThat(googleLogin.getClientName()).isEqualTo("Google"); + + ProviderDetails googleProviderDetails = googleLogin.getProviderDetails(); + assertThat(googleProviderDetails).isNotNull(); + assertThat(googleProviderDetails.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth"); + assertThat(googleProviderDetails.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token"); + assertThat(googleProviderDetails.getUserInfoEndpoint().getUri()) + .isEqualTo("https://example.com/oauth2/v3/userinfo"); + assertThat(googleProviderDetails.getUserInfoEndpoint().getAuthenticationMethod()) + .isEqualTo(AuthenticationMethod.HEADER); + assertThat(googleProviderDetails.getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo("sub"); + assertThat(googleProviderDetails.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs"); + } + + @Test + public void parseWhenMultipleClientsConfiguredThenAvailableInRepository() throws Exception { + this.spring.configLocations(this.xml("MultiClientRegistration")).autowire(); + + assertThat(clientRegistrationRepository).isInstanceOf(InMemoryClientRegistrationRepository.class); + + ClientRegistration googleLogin = clientRegistrationRepository.findByRegistrationId("google-login"); + assertThat(googleLogin).isNotNull(); + assertThat(googleLogin.getRegistrationId()).isEqualTo("google-login"); + assertThat(googleLogin.getClientId()).isEqualTo("google-client-id"); + assertThat(googleLogin.getClientSecret()).isEqualTo("google-client-secret"); + assertThat(googleLogin.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + assertThat(googleLogin.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(googleLogin.getRedirectUriTemplate()).isEqualTo("{baseUrl}/login/oauth2/code/{registrationId}"); + assertThat(googleLogin.getScopes()).isEqualTo(StringUtils.commaDelimitedListToSet("openid,profile,email")); + assertThat(googleLogin.getClientName()).isEqualTo("Google"); + + ProviderDetails googleProviderDetails = googleLogin.getProviderDetails(); + assertThat(googleProviderDetails).isNotNull(); + assertThat(googleProviderDetails.getAuthorizationUri()) + .isEqualTo("https://accounts.google.com/o/oauth2/v2/auth"); + assertThat(googleProviderDetails.getTokenUri()).isEqualTo("https://www.googleapis.com/oauth2/v4/token"); + assertThat(googleProviderDetails.getUserInfoEndpoint().getUri()) + .isEqualTo("https://www.googleapis.com/oauth2/v3/userinfo"); + assertThat(googleProviderDetails.getUserInfoEndpoint().getAuthenticationMethod()) + .isEqualTo(AuthenticationMethod.HEADER); + assertThat(googleProviderDetails.getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo("sub"); + assertThat(googleProviderDetails.getJwkSetUri()).isEqualTo("https://www.googleapis.com/oauth2/v3/certs"); + + ClientRegistration githubLogin = clientRegistrationRepository.findByRegistrationId("github-login"); + assertThat(githubLogin).isNotNull(); + assertThat(githubLogin.getRegistrationId()).isEqualTo("github-login"); + assertThat(githubLogin.getClientId()).isEqualTo("github-client-id"); + assertThat(githubLogin.getClientSecret()).isEqualTo("github-client-secret"); + assertThat(githubLogin.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + assertThat(githubLogin.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(githubLogin.getRedirectUriTemplate()).isEqualTo("{baseUrl}/login/oauth2/code/{registrationId}"); + assertThat(googleLogin.getScopes()).isEqualTo(StringUtils.commaDelimitedListToSet("openid,profile,email")); + assertThat(githubLogin.getClientName()).isEqualTo("Github"); + + ProviderDetails githubProviderDetails = githubLogin.getProviderDetails(); + assertThat(githubProviderDetails).isNotNull(); + assertThat(githubProviderDetails.getAuthorizationUri()).isEqualTo("https://github.com/login/oauth/authorize"); + assertThat(githubProviderDetails.getTokenUri()).isEqualTo("https://github.com/login/oauth/access_token"); + assertThat(githubProviderDetails.getUserInfoEndpoint().getUri()).isEqualTo("https://api.github.com/user"); + assertThat(githubProviderDetails.getUserInfoEndpoint().getAuthenticationMethod()) + .isEqualTo(AuthenticationMethod.HEADER); + assertThat(githubProviderDetails.getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo("id"); + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java b/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java index ded7134865..78422a06fc 100644 --- a/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java +++ b/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2019 the original author or authors. + * Copyright 2009-2020 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. diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration.xml new file mode 100644 index 0000000000..d0783b5af9 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationFailureHandler.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationFailureHandler.xml new file mode 100644 index 0000000000..d49e4257aa --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationFailureHandler.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationHandler.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationHandler.xml new file mode 100644 index 0000000000..fd8cbe6c4d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationHandler.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRequestResolver.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRequestResolver.xml new file mode 100644 index 0000000000..26c02c03c6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRequestResolver.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomGrantedAuthorities.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomGrantedAuthorities.xml new file mode 100644 index 0000000000..9997826ac1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomGrantedAuthorities.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginPage.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginPage.xml new file mode 100644 index 0000000000..740cc8eaf2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginPage.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginProcessingUrl.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginProcessingUrl.xml new file mode 100644 index 0000000000..531f0aa3e3 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginProcessingUrl.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithFormLogin.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithFormLogin.xml new file mode 100644 index 0000000000..766b3df927 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithFormLogin.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactory.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactory.xml new file mode 100644 index 0000000000..20f3bd9114 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactory.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler.xml new file mode 100644 index 0000000000..80633b2289 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithTestConfiguration.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithTestConfiguration.xml new file mode 100644 index 0000000000..6e69c374ec --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithTestConfiguration.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithinSameFile.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithinSameFile.xml new file mode 100644 index 0000000000..d176bfdd31 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithinSameFile.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration.xml index ebcb033200..2bef962eed 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration.xml @@ -1,6 +1,6 @@ + - diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizationRequestRepository.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizationRequestRepository.xml new file mode 100644 index 0000000000..3c4071ee06 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizationRequestRepository.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientRepository.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientRepository.xml new file mode 100644 index 0000000000..e3ffdc7a63 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientRepository.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientService.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientService.xml new file mode 100644 index 0000000000..593f329422 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientService.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomClientRegistrationRepository.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomClientRegistrationRepository.xml new file mode 100644 index 0000000000..c700b5fb4c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomClientRegistrationRepository.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-FromIssuerUri.xml b/config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-FromIssuerUri.xml new file mode 100644 index 0000000000..cbf4680a76 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-FromIssuerUri.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-MultiClientRegistration.xml b/config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-MultiClientRegistration.xml new file mode 100644 index 0000000000..b31e11bca4 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-MultiClientRegistration.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/oauth2/client/google-github-registration.xml b/config/src/test/resources/org/springframework/security/config/oauth2/client/google-github-registration.xml new file mode 100644 index 0000000000..fc27e86b03 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/oauth2/client/google-github-registration.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/oauth2/client/google-registration.xml b/config/src/test/resources/org/springframework/security/config/oauth2/client/google-registration.xml new file mode 100644 index 0000000000..46b88b41f0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/oauth2/client/google-registration.xml @@ -0,0 +1,44 @@ + + + + + + + + + diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc index a6d4273bf4..3c46154f11 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc @@ -891,6 +891,79 @@ The <> feature configures authentication support us * <> +[[nsa-oauth2-login-attributes]] +===== Attributes + + +[[nsa-oauth2-login-client-registration-repository-ref]] +* **client-registration-repository-ref** +Reference to `ClientRegistrationRepository`. + + +[[nsa-oauth2-login-authorized-client-repository-ref]] +* **authorized-client-repository-ref** +Reference to `OAuth2AuthorizedClientRepository`. + + +[[nsa-oauth2-login-authorized-client-service-ref]] +* **authorized-client-service-ref** +Reference to `OAuth2AuthorizedClientService`. + + +[[nsa-oauth2-login-authorization-request-repository-ref]] +* **authorization-request-repository-ref** +Reference to `AuthorizationRequestRepository`. + + +[[nsa-oauth2-login-authorization-request-resolver-ref]] +* **authorization-request-resolver-ref** +Reference to `OAuth2AuthorizationRequestResolver`. + + +[[nsa-oauth2-login-access-token-response-client-ref]] +* **access-token-response-client-ref** +Reference to `OAuth2AccessTokenResponseClient`. + + +[[nsa-oauth2-login-user-authorities-mapper-ref]] +* **user-authorities-mapper-ref** +Reference to `GrantedAuthoritiesMapper`. + + +[[nsa-oauth2-login-user-service-ref]] +* **user-service-ref** +Reference to `OAuth2UserService`. + + +[[nsa-oauth2-login-oidc-user-service-ref]] +* **oidc-user-service-ref** +Reference to `OidcUserService`. + + +[[nsa-oauth2-login-login-processing-url]] +* **login-processing-url** +Specifies the URL to validate the credentials. + + +[[nsa-oauth2-login-login-page]] +* **login-page** +Specifies the URL to send users to if login is required + + +[[nsa-oauth2-login-authentication-success-handler-ref]] +* **authentication-success-handler-ref** +Specifies authentication success handler + + +[[nsa-oauth2-login-authentication-failure-handler-ref]] +* **authentication-failure-handler-ref** +Specifies authentication failure handler + + +[[nsa-oauth2-login-jwt-decoder-factory-ref]] +* **jwt-decoder-factory-ref** +Specifies JWT decoder factory for OidcAuthorizationCodeAuthenticationProvider + [[nsa-client-registrations]] ====