1
0
mirror of synced 2026-08-04 09:17:02 +00:00

Add support for OAuth 2.0 Login

Fixes gh-3907
This commit is contained in:
Joe Grandja
2017-03-20 16:18:08 -04:00
parent a38352c4cc
commit 829c386756
81 changed files with 6483 additions and 48 deletions
+7
View File
@@ -44,6 +44,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.0.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
+7
View File
@@ -44,6 +44,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.0.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
+342
View File
@@ -0,0 +1,342 @@
= OAuth 2.0 Login Sample
Joe Grandja
:toc:
:security-site-url: https://projects.spring.io/spring-security/
[.lead]
This guide will walk you through the steps for setting up the sample application with OAuth 2.0 Login using an external _OAuth 2.0_ or _OpenID Connect 1.0_ Provider.
The sample application is built with *Spring Boot 1.5* and the *spring-security-oauth2-client* module that is new in {security-site-url}[Spring Security 5.0].
The following sections outline detailed steps for setting up OAuth 2.0 Login with these Providers:
* <<google-login, Google>>
* <<github-login, GitHub>>
* <<facebook-login, Facebook>>
* <<okta-login, Okta>>
NOTE: The _"authentication flow"_ is realized using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework].
[[sample-app-content]]
== Sample application content
The sample application contains the following package structure and artifacts:
*org.springframework.security.samples*
[circle]
* _OAuth2LoginApplication_ - the main class for the _Spring application_.
** *user*
*** _GitHubOAuth2User_ - a custom _UserInfo_ type for <<github-login, GitHub Login>>.
** *web*
*** _MainController_ - the root controller that displays user information after a successful login.
*org.springframework.boot.autoconfigure.security.oauth2.client*
[circle]
* <<client-registration-auto-configuration-class, _ClientRegistrationAutoConfiguration_>> - a Spring Boot auto-configuration class
that automatically registers a _ClientRegistrationRepository_ bean in the _ApplicationContext_.
* <<oauth2-login-auto-configuration-class, _OAuth2LoginAutoConfiguration_>> - a Spring Boot auto-configuration class that automatically enables OAuth 2.0 Login.
WARNING: The Spring Boot auto-configuration classes (and dependent resources) will eventually _live_ in the *Spring Boot Security Starter*.
NOTE: See <<oauth2-login-auto-configuration, OAuth 2.0 Login auto-configuration>> for a detailed overview of the auto-configuration classes.
[[google-login]]
== Setting up *_Login with Google_*
The goal for this section of the guide is to setup login using Google as the _Authentication Provider_.
NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the
http://openid.net/connect/[OpenID Connect] specification and is http://openid.net/certification/[OpenID Certified].
[[google-login-register-credentials]]
=== Register OAuth 2.0 credentials
In order to use Google's OAuth 2.0 authentication system for login, you must set up a project in the *Google API Console* to obtain OAuth 2.0 credentials.
Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page starting in the section *_"Setting up OAuth 2.0"_*.
After completing the sub-section, *_"Obtain OAuth 2.0 credentials"_*, you should have created a new *OAuth Client* with credentials consisting of a *Client ID* and *Client Secret*.
[[google-login-redirect-uri]]
=== Setting the redirect URI
The redirect URI is the path in the sample application that the end-user's user-agent is redirected back to after they have authenticated with Google
and have granted access to the OAuth Client _(created from the <<google-login-register-credentials, previous step>>)_ on the *Consent screen* page.
For the sub-section, *_"Set a redirect URI"_*, ensure the *Authorised redirect URIs* is set to *http://localhost:8080/oauth2/authorize/code/google*
TIP: The default redirect URI is *_"{scheme}://{serverName}:{serverPort}/oauth2/authorize/code/{clientAlias}"_*.
See <<oauth2-client-properties, OAuth client properties>> for more details on this default.
[[google-login-configure-application-yml]]
=== Configuring application.yml
Now that we have created a new OAuth Client with Google, we need to configure the sample application to use this OAuth Client for the _authentication flow_.
Go to *_src/main/resources_* and edit *application.yml*. Add the following configuration:
[source,yaml]
----
security:
oauth2:
client:
google:
client-id: ${client-id}
client-secret: ${client-secret}
----
Replace *${client-id}* and *${client-secret}* with the OAuth 2.0 credentials created in the previous section <<google-login-register-credentials, Register OAuth 2.0 credentials>>.
[TIP]
.OAuth client properties
====
. *security.oauth2.client* is the *_base property prefix_* for OAuth client properties.
. Just below the *_base property prefix_* is the *_client property key_*, for example *security.oauth2.client.google*.
. At the base of the *_client property key_* are the properties for specifying the configuration for an OAuth Client.
A list of these properties are detailed in <<oauth2-client-properties, OAuth client properties>>.
====
[[google-login-run-sample]]
=== Running the sample
Launch the Spring Boot application by running *_org.springframework.security.samples.OAuth2LoginApplication_*.
After the application successfully starts up, go to http://localhost:8080. You will be redirected to http://localhost:8080/login, which will display an _auto-generated login page_ with an anchor link for *Google*.
Click through on the Google link and you'll be redirected to Google for authentication.
After you authenticate using your Google credentials, the next page presented to you will be the *Consent screen*.
The Consent screen will ask you to either *_Allow_* or *_Deny_* access to the OAuth Client you created in the previous step <<google-login-register-credentials, Register OAuth 2.0 credentials>>.
Click *_Allow_* to authorize the OAuth Client to access your _email address_ and _basic profile_ information.
At this point, the OAuth Client will retrieve your email address and basic profile information from the http://openid.net/specs/openid-connect-core-1_0.html#UserInfo[*UserInfo Endpoint*] and establish an _authenticated session_.
The home page will then be displayed showing the user attributes retrieved from the *UserInfo Endpoint*, for example, name, email, profile, sub, etc.
[[oauth2-login-auto-configuration]]
== OAuth 2.0 Login auto-configuration
As you worked through this guide and setup OAuth 2.0 Login with one of the Providers,
we hope you noticed the ease in configuration and setup required in getting the sample up and running?
And you may be asking, how does this all work? Thanks to some Spring Boot auto-configuration _magic_,
we were able to automatically register the OAuth Client(s) configured in the `Environment`,
as well, provide a minimal security configuration for OAuth 2.0 Login for these registered OAuth Client(s).
The following provides an overview of the Spring Boot auto-configuration classes:
[[client-registration-auto-configuration-class]]
*_org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration_*::
`ClientRegistrationAutoConfiguration` is responsible for registering a `ClientRegistrationRepository` _bean_ with the `ApplicationContext`.
The `ClientRegistrationRepository` is composed of one or more `ClientRegistration` instances, which are created from the OAuth client properties
configured in the `Environment` that are prefixed with `security.oauth2.client.[client-alias]`, for example, `security.oauth2.client.google`.
NOTE: `ClientRegistrationAutoConfiguration` also loads a _resource_ named *oauth2-clients-defaults.yml*,
which provides a set of default client property values for a number of _well-known_ Providers.
More on this in the later section <<oauth2-default-client-properties, Default client property values>>.
[[oauth2-login-auto-configuration-class]]
*_org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2LoginAutoConfiguration_*::
`OAuth2LoginAutoConfiguration` is responsible for enabling OAuth 2.0 Login,
only if there is a `ClientRegistrationRepository` _bean_ available in the `ApplicationContext`.
WARNING: The auto-configuration classes (and dependent resources) will eventually _live_ in the *Spring Boot Security Starter*.
[[oauth2-client-properties]]
=== OAuth client properties
The following specifies the common set of properties available for configuring an OAuth Client.
[TIP]
====
- *security.oauth2.client* is the *_base property prefix_* for OAuth client properties.
- Just below the *_base property prefix_* is the *_client property key_*, for example *security.oauth2.client.google*.
- At the base of the *_client property key_* are the properties for specifying the configuration for an OAuth Client.
====
- *client-authentication-method* - the method used to authenticate the _Client_ with the _Provider_. Supported values are *header* and *form*.
- *authorized-grant-type* - the OAuth 2.0 Authorization Framework defines the https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] grant type,
which is used to realize the _"authentication flow"_. Currently, this is the only supported grant type.
- *redirect-uri* - this is the client's _registered_ redirect URI that the _Authorization Server_ redirects the end-user's user-agent
to after the end-user has authenticated and authorized access for the client.
NOTE: The default redirect URI is _"{scheme}://{serverName}:{serverPort}/oauth2/authorize/code/{clientAlias}"_, which leverages *URI template variables*.
- *scopes* - a comma-delimited string of scope(s) requested during the _Authorization Request_ flow, for example: _openid, email, profile_
NOTE: _OpenID Connect 1.0_ defines these http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims[standard scopes]: _profile, email, address, phone_
NOTE: Non-standard scopes may be defined by a standard _OAuth 2.0 Provider_. Please consult the Provider's OAuth API documentation to learn which scopes are supported.
- *authorization-uri* - the URI used by the client to redirect the end-user's user-agent to the _Authorization Server_ in order to obtain authorization from the end-user (the _Resource Owner_).
- *token-uri* - the URI used by the client when exchanging an _Authorization Grant_ (for example, Authorization Code) for an _Access Token_ at the _Authorization Server_.
- *user-info-uri* - the URI used by the client to access the protected resource *UserInfo Endpoint*, in order to obtain attributes of the end-user.
- *user-info-converter* - the `Converter` implementation class used to convert the *UserInfo Response* to a `UserInfo` (_OpenID Connect 1.0 Provider_) or `OAuth2User` instance (_Standard OAuth 2.0 Provider_).
TIP: The `Converter` implementation class for an _OpenID Connect 1.0 Provider_ is *org.springframework.security.oauth2.client.user.converter.UserInfoConverter*
and for a standard _OAuth 2.0 Provider_ it's *org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter*.
- *user-info-name-attribute-key* - the _key_ used to retrieve the *Name* of the end-user from the `Map` of available attributes in `UserInfo` or `OAuth2User`.
NOTE: _OpenID Connect 1.0_ defines the http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims[*"name"* Claim], which is the end-user's full name and is the default used for `UserInfo`.
IMPORTANT: Standard _OAuth 2.0 Provider's_ may vary the naming of their *Name* attribute. Please consult the Provider's *UserInfo* API documentation.
This is a *_required_* property when *user-info-converter* is set to `OAuth2UserConverter`.
- *client-name* - this is a descriptive name used for the client. The name may be used in certain scenarios, for example, when displaying the name of the client in the _auto-generated login page_.
- *client-alias* - an _alias_ which uniquely identifies the client. It *must be* unique within a `ClientRegistrationRepository`.
[[oauth2-default-client-properties]]
=== Default client property values
As noted previously, <<client-registration-auto-configuration-class, `ClientRegistrationAutoConfiguration`>> loads a _resource_ named *oauth2-clients-defaults.yml*,
which provides a set of default client property values for a number of _well-known_ Providers.
For example, the *authorization-uri*, *token-uri*, *user-info-uri* rarely change for a Provider and therefore it makes sense to
provide a set of defaults in order to reduce the configuration required by the user.
Below are the current set of default client property values:
.oauth2-clients-defaults.yml
[source,yaml]
----
security:
oauth2:
client:
google:
client-authentication-method: header
authorized-grant-type: authorization_code
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
scopes: openid, email, profile
authorization-uri: "https://accounts.google.com/o/oauth2/auth"
token-uri: "https://accounts.google.com/o/oauth2/token"
user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
client-name: Google
client-alias: google
github:
client-authentication-method: header
authorized-grant-type: authorization_code
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
scopes: user
authorization-uri: "https://github.com/login/oauth/authorize"
token-uri: "https://github.com/login/oauth/access_token"
user-info-uri: "https://api.github.com/user"
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
client-name: GitHub
client-alias: github
facebook:
client-authentication-method: form
authorized-grant-type: authorization_code
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
scopes: public_profile, email
authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth"
token-uri: "https://graph.facebook.com/v2.8/oauth/access_token"
user-info-uri: "https://graph.facebook.com/me"
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
client-name: Facebook
client-alias: facebook
okta:
client-authentication-method: header
authorized-grant-type: authorization_code
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
scopes: openid, email, profile
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
client-name: Okta
client-alias: okta
----
= Appendix
'''
[[configure-non-spring-boot-app]]
== Configuring a _Non-Spring-Boot_ application
If you are not using Spring Boot for your application, you will not be able to leverage the auto-configuration features for OAuth 2.0 Login.
You will be required to provide your own _security configuration_ in order to enable OAuth 2.0 Login.
The following sample code demonstrates a minimal security configuration for enabling OAuth 2.0 Login.
Assuming we have a _properties file_ named *oauth2-clients.properties* on the _classpath_ and it specifies all the _required_ properties for an OAuth Client, specifically _"Google"_:
.oauth2-clients.properties
[source,properties]
----
security.oauth2.client.google.client-id=${client-id}
security.oauth2.client.google.client-secret=${client-secret}
security.oauth2.client.google.client-authentication-method=header
security.oauth2.client.google.authorized-grant-type=authorization_code
security.oauth2.client.google.redirect-uri=http://localhost:8080/oauth2/authorize/code/google
security.oauth2.client.google.scopes=openid,email,profile
security.oauth2.client.google.authorization-uri=https://accounts.google.com/o/oauth2/auth
security.oauth2.client.google.token-uri=https://accounts.google.com/o/oauth2/token
security.oauth2.client.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
security.oauth2.client.google.user-info-converter=org.springframework.security.oauth2.client.user.converter.UserInfoConverter
security.oauth2.client.google.client-name=Google
security.oauth2.client.google.client-alias=google
----
The following _security configuration_ will enable OAuth 2.0 Login using _"Google"_ as the _Authentication Provider_:
[source,java]
----
@EnableWebSecurity
@PropertySource("classpath:oauth2-clients.properties")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private Environment environment;
public SecurityConfig(Environment environment) {
this.environment = environment;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.clients(clientRegistrationRepository())
.userInfoEndpoint()
.userInfoTypeConverter(
new UserInfoConverter(),
new URI("https://www.googleapis.com/oauth2/v3/userinfo"));
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> clientRegistrations = Collections.singletonList(
clientRegistration("security.oauth2.client.google."));
return new InMemoryClientRegistrationRepository(clientRegistrations);
}
private ClientRegistration clientRegistration(String clientPropertyKey) {
String clientId = this.environment.getProperty(clientPropertyKey + "client-id");
String clientSecret = this.environment.getProperty(clientPropertyKey + "client-secret");
ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.valueOf(
this.environment.getProperty(clientPropertyKey + "client-authentication-method").toUpperCase());
AuthorizationGrantType authorizationGrantType = AuthorizationGrantType.valueOf(
this.environment.getProperty(clientPropertyKey + "authorized-grant-type").toUpperCase());
String redirectUri = this.environment.getProperty(clientPropertyKey + "redirect-uri");
String[] scopes = this.environment.getProperty(clientPropertyKey + "scopes").split(",");
String authorizationUri = this.environment.getProperty(clientPropertyKey + "authorization-uri");
String tokenUri = this.environment.getProperty(clientPropertyKey + "token-uri");
String userInfoUri = this.environment.getProperty(clientPropertyKey + "user-info-uri");
String clientName = this.environment.getProperty(clientPropertyKey + "client-name");
String clientAlias = this.environment.getProperty(clientPropertyKey + "client-alias");
return new ClientRegistration.Builder(clientId)
.clientSecret(clientSecret)
.clientAuthenticationMethod(clientAuthenticationMethod)
.authorizedGrantType(authorizationGrantType)
.redirectUri(redirectUri)
.scopes(scopes)
.authorizationUri(authorizationUri)
.tokenUri(tokenUri)
.userInfoUri(userInfoUri)
.clientName(clientName)
.clientAlias(clientAlias)
.build();
}
}
----
+173
View File
@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-samples-boot-oauth2login</artifactId>
<version>5.0.0.BUILD-SNAPSHOT</version>
<name>spring-security-samples-boot-oauth2login</name>
<description>spring-security-samples-boot-oauth2login</description>
<url>http://spring.io/spring-security</url>
<organization>
<name>spring.io</name>
<url>http://spring.io/</url>
</organization>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>rwinch</id>
<name>Rob Winch</name>
<email>rwinch@pivotal.io</email>
</developer>
<developer>
<id>jgrandja</id>
<name>Joe Grandja</name>
<email>jgrandja@pivotal.io</email>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/spring-projects/spring-security</connection>
<developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
<url>https://github.com/spring-projects/spring-security</url>
</scm>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>4.3.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.0.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.0.BUILD-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
<version>5.0.0.BUILD-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.0.BUILD-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>2.1.3.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.24</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>5.0.0.BUILD-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-snapshot</id>
<url>https://repo.spring.io/snapshot</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,15 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-security-config')
compile project(':spring-security-oauth2-client')
compile project(':spring-security-web')
compile 'org.springframework.boot:spring-boot-starter-security'
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
compile 'org.springframework.boot:spring-boot-starter-web'
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
testCompile project(':spring-security-test')
testCompile 'net.sourceforge.htmlunit:htmlunit'
testCompile 'org.springframework.boot:spring-boot-starter-test'
}
@@ -0,0 +1,405 @@
/*
* Copyright 2012-2017 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.samples;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.html.DomNodeList;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter;
import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.user.OAuth2UserService;
import org.springframework.security.oauth2.core.AccessToken;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
import org.springframework.security.oauth2.core.endpoint.ResponseType;
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Integration tests for the OAuth 2.0 client filters {@link AuthorizationCodeRequestRedirectFilter}
* and {@link AuthorizationCodeAuthenticationProcessingFilter}.
* These filters work together to realize the Authorization Code Grant flow.
*
* @author Joe Grandja
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class OAuth2LoginApplicationTests {
private static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code";
private static final String AUTHORIZE_BASE_URL = "http://localhost:8080/oauth2/authorize/code";
@Autowired
private WebClient webClient;
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
private ClientRegistration googleClientRegistration;
private ClientRegistration githubClientRegistration;
private ClientRegistration facebookClientRegistration;
private ClientRegistration oktaClientRegistration;
@Before
public void setup() {
this.webClient.getCookieManager().clearCookies();
this.googleClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("google");
this.githubClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("github");
this.facebookClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("facebook");
this.oktaClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("okta");
}
@Test
public void requestRootPageWhenNotAuthenticatedThenDisplayLoginPage() throws Exception {
HtmlPage page = this.webClient.getPage("/");
this.assertLoginPage(page);
}
@Test
public void requestOtherPageWhenNotAuthenticatedThenDisplayLoginPage() throws Exception {
HtmlPage page = this.webClient.getPage("/other-page");
this.assertLoginPage(page);
}
@Test
public void requestAuthorizeGitHubClientWhenLinkClickedThenStatusRedirectForAuthorization() throws Exception {
HtmlPage page = this.webClient.getPage("/");
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.githubClientRegistration);
assertThat(clientAnchorElement).isNotNull();
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.MOVED_PERMANENTLY.value());
String authorizeRedirectUri = response.getResponseHeaderValue("Location");
assertThat(authorizeRedirectUri).isNotNull();
UriComponents uriComponents = UriComponentsBuilder.fromUri(URI.create(authorizeRedirectUri)).build();
String requestUri = uriComponents.getScheme() + "://" + uriComponents.getHost() + uriComponents.getPath();
assertThat(requestUri).isEqualTo(this.githubClientRegistration.getProviderDetails().getAuthorizationUri().toString());
Map<String, String> params = uriComponents.getQueryParams().toSingleValueMap();
assertThat(params.get(OAuth2Parameter.RESPONSE_TYPE)).isEqualTo(ResponseType.CODE.value());
assertThat(params.get(OAuth2Parameter.CLIENT_ID)).isEqualTo(this.githubClientRegistration.getClientId());
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
assertThat(URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8")).isEqualTo(redirectUri);
assertThat(URLDecoder.decode(params.get(OAuth2Parameter.SCOPE), "UTF-8"))
.isEqualTo(this.githubClientRegistration.getScopes().stream().collect(Collectors.joining(" ")));
assertThat(params.get(OAuth2Parameter.STATE)).isNotNull();
}
@Test
public void requestAuthorizeClientWhenInvalidClientThenStatusBadRequest() throws Exception {
HtmlPage page = this.webClient.getPage("/");
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
assertThat(clientAnchorElement).isNotNull();
clientAnchorElement.setAttribute("href", clientAnchorElement.getHrefAttribute() + "-invalid");
WebResponse response = null;
try {
clientAnchorElement.click();
} catch (FailingHttpStatusCodeException ex) {
response = ex.getResponse();
}
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestAuthorizationCodeGrantWhenValidAuthorizationResponseThenDisplayUserInfoPage() throws Exception {
HtmlPage page = this.webClient.getPage("/");
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.githubClientRegistration);
assertThat(clientAnchorElement).isNotNull();
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
URI.create(response.getResponseHeaderValue("Location"))).build();
Map<String, String> params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
String code = "auth-code";
String state = URLDecoder.decode(params.get(OAuth2Parameter.STATE), "UTF-8");
String redirectUri = URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8");
String authorizationResponseUri =
UriComponentsBuilder.fromHttpUrl(redirectUri)
.queryParam(OAuth2Parameter.CODE, code)
.queryParam(OAuth2Parameter.STATE, state)
.build().encode().toUriString();
page = this.webClient.getPage(new URL(authorizationResponseUri));
this.assertUserInfoPage(page);
}
@Test
public void requestAuthorizationCodeGrantWhenNoMatchingAuthorizationRequestThenDisplayLoginPageWithError() throws Exception {
HtmlPage page = this.webClient.getPage("/");
URL loginPageUrl = page.getBaseURL();
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
String code = "auth-code";
String state = "state";
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.googleClientRegistration.getClientAlias();
String authorizationResponseUri =
UriComponentsBuilder.fromHttpUrl(redirectUri)
.queryParam(OAuth2Parameter.CODE, code)
.queryParam(OAuth2Parameter.STATE, state)
.build().encode().toUriString();
// Clear session cookie will ensure the 'session-saved'
// Authorization Request (from previous request) is not found
this.webClient.getCookieManager().clearCookies();
page = this.webClient.getPage(new URL(authorizationResponseUri));
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
assertThat(errorElement).isNotNull();
assertThat(errorElement.asText()).contains("authorization_request_not_found");
}
@Test
public void requestAuthorizationCodeGrantWhenInvalidStateParamThenDisplayLoginPageWithError() throws Exception {
HtmlPage page = this.webClient.getPage("/");
URL loginPageUrl = page.getBaseURL();
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
assertThat(clientAnchorElement).isNotNull();
this.followLinkDisableRedirects(clientAnchorElement);
String code = "auth-code";
String state = "invalid-state";
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
String authorizationResponseUri =
UriComponentsBuilder.fromHttpUrl(redirectUri)
.queryParam(OAuth2Parameter.CODE, code)
.queryParam(OAuth2Parameter.STATE, state)
.build().encode().toUriString();
page = this.webClient.getPage(new URL(authorizationResponseUri));
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
assertThat(errorElement).isNotNull();
assertThat(errorElement.asText()).contains("invalid_state_parameter");
}
@Test
public void requestAuthorizationCodeGrantWhenInvalidRedirectUriThenDisplayLoginPageWithError() throws Exception {
HtmlPage page = this.webClient.getPage("/");
URL loginPageUrl = page.getBaseURL();
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
assertThat(clientAnchorElement).isNotNull();
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
URI.create(response.getResponseHeaderValue("Location"))).build();
Map<String, String> params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
String code = "auth-code";
String state = URLDecoder.decode(params.get(OAuth2Parameter.STATE), "UTF-8");
String redirectUri = URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8");
redirectUri += "-invalid";
String authorizationResponseUri =
UriComponentsBuilder.fromHttpUrl(redirectUri)
.queryParam(OAuth2Parameter.CODE, code)
.queryParam(OAuth2Parameter.STATE, state)
.build().encode().toUriString();
page = this.webClient.getPage(new URL(authorizationResponseUri));
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
assertThat(errorElement).isNotNull();
assertThat(errorElement.asText()).contains("invalid_redirect_uri_parameter");
}
@Test
public void requestAuthorizationCodeGrantWhenStandardErrorCodeResponseThenDisplayLoginPageWithError() throws Exception {
HtmlPage page = this.webClient.getPage("/");
URL loginPageUrl = page.getBaseURL();
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
String error = OAuth2Error.INVALID_CLIENT_ERROR_CODE;
String state = "state";
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
String authorizationResponseUri =
UriComponentsBuilder.fromHttpUrl(redirectUri)
.queryParam(OAuth2Parameter.ERROR, error)
.queryParam(OAuth2Parameter.STATE, state)
.build().encode().toUriString();
page = this.webClient.getPage(new URL(authorizationResponseUri));
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
assertThat(errorElement).isNotNull();
assertThat(errorElement.asText()).contains(error);
}
private void assertLoginPage(HtmlPage page) throws Exception {
assertThat(page.getTitleText()).isEqualTo("Login Page");
int expectedClients = 4;
List<HtmlAnchor> clientAnchorElements = page.getAnchors();
assertThat(clientAnchorElements.size()).isEqualTo(expectedClients);
String baseAuthorizeUri = AUTHORIZATION_BASE_URI + "/";
String googleClientAuthorizeUri = baseAuthorizeUri + this.googleClientRegistration.getClientAlias();
String githubClientAuthorizeUri = baseAuthorizeUri + this.githubClientRegistration.getClientAlias();
String facebookClientAuthorizeUri = baseAuthorizeUri + this.facebookClientRegistration.getClientAlias();
String oktaClientAuthorizeUri = baseAuthorizeUri + this.oktaClientRegistration.getClientAlias();
for (int i=0; i<expectedClients; i++) {
assertThat(clientAnchorElements.get(i).getAttribute("href")).isIn(
googleClientAuthorizeUri, githubClientAuthorizeUri,
facebookClientAuthorizeUri, oktaClientAuthorizeUri);
assertThat(clientAnchorElements.get(i).asText()).isIn(
this.googleClientRegistration.getClientName(),
this.githubClientRegistration.getClientName(),
this.facebookClientRegistration.getClientName(),
this.oktaClientRegistration.getClientName());
}
}
private void assertUserInfoPage(HtmlPage page) throws Exception {
assertThat(page.getTitleText()).isEqualTo("Spring Security - OAuth2 User Info");
DomNodeList<HtmlElement> divElements = page.getBody().getElementsByTagName("div");
assertThat(divElements.get(1).asText()).contains("User: joeg@springsecurity.io");
assertThat(divElements.get(4).asText()).contains("Name: joeg@springsecurity.io");
}
private HtmlAnchor getClientAnchorElement(HtmlPage page, ClientRegistration clientRegistration) {
Optional<HtmlAnchor> clientAnchorElement = page.getAnchors().stream()
.filter(e -> e.asText().equals(clientRegistration.getClientName())).findFirst();
return (clientAnchorElement.isPresent() ? clientAnchorElement.get() : null);
}
private WebResponse followLinkDisableRedirects(HtmlAnchor anchorElement) throws Exception {
WebResponse response = null;
try {
// Disable the automatic redirection (which will trigger
// an exception) so that we can capture the response
this.webClient.getOptions().setRedirectEnabled(false);
anchorElement.click();
} catch (FailingHttpStatusCodeException ex) {
response = ex.getResponse();
this.webClient.getOptions().setRedirectEnabled(true);
}
return response;
}
@EnableWebSecurity
public static class SecurityTestConfig extends WebSecurityConfigurerAdapter {
// @formatter:off
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.authorizationCodeTokenExchanger(this.mockAuthorizationCodeTokenExchanger())
.userInfoEndpoint()
.userInfoService(this.mockUserInfoService());
}
// @formatter:on
private AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> mockAuthorizationCodeTokenExchanger() {
TokenResponseAttributes tokenResponse = TokenResponseAttributes.withToken("access-token-1234")
.tokenType(AccessToken.TokenType.BEARER)
.expiresIn(60 * 1000)
.scopes(Collections.singleton("openid"))
.build();
AuthorizationGrantTokenExchanger mock = mock(AuthorizationGrantTokenExchanger.class);
when(mock.exchange(any())).thenReturn(tokenResponse);
return mock;
}
private OAuth2UserService mockUserInfoService() {
Map<String, Object> attributes = new HashMap<>();
attributes.put("id", "joeg");
attributes.put("first-name", "Joe");
attributes.put("last-name", "Grandja");
attributes.put("email", "joeg@springsecurity.io");
DefaultOAuth2User user = new DefaultOAuth2User(attributes, "email");
OAuth2UserService mock = mock(OAuth2UserService.class);
when(mock.loadUser(any())).thenReturn(user);
return mock;
}
}
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(basePackages = "org.springframework.security.samples.web")
public static class SpringBootApplicationTestConfig {
}
}
@@ -0,0 +1,129 @@
/*
* Copyright 2012-2017 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.security.oauth2.client;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.bind.PropertySourcesBinder;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.annotation.*;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationProperties;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Joe Grandja
*/
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(ClientRegistrationRepository.class)
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
@AutoConfigureBefore(SecurityAutoConfiguration.class)
public class ClientRegistrationAutoConfiguration {
private static final String CLIENT_ID_PROPERTY = "client-id";
private static final String CLIENTS_DEFAULTS_RESOURCE = "META-INF/oauth2-clients-defaults.yml";
static final String CLIENT_PROPERTY_PREFIX = "security.oauth2.client.";
@Configuration
@Conditional(ClientPropertiesAvailableCondition.class)
protected static class ClientRegistrationConfiguration {
private final Environment environment;
protected ClientRegistrationConfiguration(Environment environment) {
this.environment = environment;
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
Properties clientsDefaultProperties = this.getClientsDefaultProperties();
if (clientsDefaultProperties != null) {
propertySources.addLast(new PropertiesPropertySource("oauth2ClientsDefaults", clientsDefaultProperties));
}
PropertySourcesBinder binder = new PropertySourcesBinder(propertySources);
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(this.environment, CLIENT_PROPERTY_PREFIX);
List<ClientRegistration> clientRegistrations = new ArrayList<>();
Set<String> clientPropertyKeys = resolveClientPropertyKeys(this.environment);
for (String clientPropertyKey : clientPropertyKeys) {
if (!resolver.containsProperty(clientPropertyKey + "." + CLIENT_ID_PROPERTY)) {
continue;
}
ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties();
binder.bindTo(CLIENT_PROPERTY_PREFIX + clientPropertyKey, clientRegistrationProperties);
ClientRegistration clientRegistration = new ClientRegistration.Builder(clientRegistrationProperties).build();
clientRegistrations.add(clientRegistration);
}
return new InMemoryClientRegistrationRepository(clientRegistrations);
}
private Properties getClientsDefaultProperties() {
ClassPathResource clientsDefaultsResource = new ClassPathResource(CLIENTS_DEFAULTS_RESOURCE);
if (!clientsDefaultsResource.exists()) {
return null;
}
YamlPropertiesFactoryBean yamlPropertiesFactory = new YamlPropertiesFactoryBean();
yamlPropertiesFactory.setResources(clientsDefaultsResource);
return yamlPropertiesFactory.getObject();
}
}
static Set<String> resolveClientPropertyKeys(Environment environment) {
Set<String> clientPropertyKeys = new LinkedHashSet<>();
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, CLIENT_PROPERTY_PREFIX);
resolver.getSubProperties("").keySet().forEach(key -> {
int endIndex = key.indexOf('.');
if (endIndex != -1) {
clientPropertyKeys.add(key.substring(0, endIndex));
}
});
return clientPropertyKeys;
}
private static class ClientPropertiesAvailableCondition extends SpringBootCondition implements ConfigurationCondition {
@Override
public ConfigurationCondition.ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.PARSE_CONFIGURATION;
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth2 Client Properties");
Set<String> clientPropertyKeys = resolveClientPropertyKeys(context.getEnvironment());
if (!CollectionUtils.isEmpty(clientPropertyKeys)) {
return ConditionOutcome.match(message.foundExactly("OAuth2 Client(s) -> " +
clientPropertyKeys.stream().collect(Collectors.joining(", "))));
}
return ConditionOutcome.noMatch(message.notAvailable("OAuth2 Client(s)"));
}
}
}
@@ -0,0 +1,108 @@
/*
* Copyright 2012-2017 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.security.oauth2.client;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.env.Environment;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.user.converter.AbstractOAuth2UserConverter;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.util.Set;
import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.CLIENT_PROPERTY_PREFIX;
import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.resolveClientPropertyKeys;
/**
* @author Joe Grandja
*/
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(EnableWebSecurity.class)
@ConditionalOnMissingBean(WebSecurityConfiguration.class)
@ConditionalOnBean(ClientRegistrationRepository.class)
@AutoConfigureBefore(SecurityAutoConfiguration.class)
@AutoConfigureAfter(ClientRegistrationAutoConfiguration.class)
public class OAuth2LoginAutoConfiguration {
private static final String USER_INFO_URI_PROPERTY = "user-info-uri";
private static final String USER_INFO_CONVERTER_PROPERTY = "user-info-converter";
private static final String USER_INFO_NAME_ATTR_KEY_PROPERTY = "user-info-name-attribute-key";
@EnableWebSecurity
protected static class OAuth2LoginSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final Environment environment;
protected OAuth2LoginSecurityConfiguration(Environment environment) {
this.environment = environment;
}
// @formatter:off
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/favicon.ico").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login();
this.registerUserInfoTypeConverters(http.oauth2Login());
}
// @formatter:on
private void registerUserInfoTypeConverters(OAuth2LoginConfigurer<HttpSecurity> oauth2LoginConfigurer) throws Exception {
Set<String> clientPropertyKeys = resolveClientPropertyKeys(this.environment);
for (String clientPropertyKey : clientPropertyKeys) {
String fullClientPropertyKey = CLIENT_PROPERTY_PREFIX + clientPropertyKey + ".";
String userInfoUriValue = this.environment.getProperty(fullClientPropertyKey + USER_INFO_URI_PROPERTY);
String userInfoConverterTypeValue = this.environment.getProperty(fullClientPropertyKey + USER_INFO_CONVERTER_PROPERTY);
if (userInfoUriValue != null && userInfoConverterTypeValue != null) {
Class<? extends Converter> userInfoConverterType = ClassUtils.resolveClassName(
userInfoConverterTypeValue, this.getClass().getClassLoader()).asSubclass(Converter.class);
Converter<ClientHttpResponse, ? extends OAuth2User> userInfoConverter = null;
if (AbstractOAuth2UserConverter.class.isAssignableFrom(userInfoConverterType)) {
Constructor<? extends Converter> oauth2UserConverterConstructor = ClassUtils.getConstructorIfAvailable(userInfoConverterType, String.class);
if (oauth2UserConverterConstructor != null) {
String userInfoNameAttributeKey = this.environment.getProperty(fullClientPropertyKey + USER_INFO_NAME_ATTR_KEY_PROPERTY);
userInfoConverter = (Converter<ClientHttpResponse, ? extends OAuth2User>)oauth2UserConverterConstructor.newInstance(userInfoNameAttributeKey);
}
}
if (userInfoConverter == null) {
userInfoConverter = (Converter<ClientHttpResponse, ? extends OAuth2User>)userInfoConverterType.newInstance();
}
oauth2LoginConfigurer.userInfoEndpoint().userInfoTypeConverter(userInfoConverter, new URI(userInfoUriValue));
}
}
}
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2012-2017 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.samples;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Joe Grandja
*/
@SpringBootApplication
public class OAuth2LoginApplication {
public OAuth2LoginApplication() {
}
public static void main(String[] args) {
SpringApplication.run(OAuth2LoginApplication.class, args);
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2012-2017 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.samples.user;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* @author Joe Grandja
*/
public class GitHubOAuth2User implements OAuth2User {
private String id;
private String name;
private String login;
private String email;
public GitHubOAuth2User() {
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptyList();
}
@Override
public Map<String, Object> getAttributes() {
Map<String, Object> attributes = new HashMap<>();
attributes.put("id", this.getId());
attributes.put("name", this.getName());
attributes.put("login", this.getLogin());
attributes.put("email", this.getEmail());
return attributes;
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
@Override
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getLogin() {
return this.login;
}
public void setLogin(String login) {
this.login = login;
}
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2012-2017 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.samples.web;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author Joe Grandja
*/
@Controller
public class MainController {
@RequestMapping("/")
public String index(Model model, @AuthenticationPrincipal OAuth2User user) {
model.addAttribute("userName", user.getName());
model.addAttribute("userAttributes", user.getAttributes());
return "index";
}
}
@@ -0,0 +1,44 @@
security:
oauth2:
client:
google:
client-authentication-method: header
authorized-grant-type: authorization_code
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
scopes: openid, email, profile
authorization-uri: "https://accounts.google.com/o/oauth2/auth"
token-uri: "https://accounts.google.com/o/oauth2/token"
user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
client-name: Google
client-alias: google
github:
client-authentication-method: header
authorized-grant-type: authorization_code
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
scopes: user
authorization-uri: "https://github.com/login/oauth/authorize"
token-uri: "https://github.com/login/oauth/access_token"
user-info-uri: "https://api.github.com/user"
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
client-name: GitHub
client-alias: github
facebook:
client-authentication-method: form
authorized-grant-type: authorization_code
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
scopes: public_profile, email
authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth"
token-uri: "https://graph.facebook.com/v2.8/oauth/access_token"
user-info-uri: "https://graph.facebook.com/me"
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
client-name: Facebook
client-alias: facebook
okta:
client-authentication-method: header
authorized-grant-type: authorization_code
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
scopes: openid, email, profile
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
client-name: Okta
client-alias: okta
@@ -0,0 +1,4 @@
# Spring Boot Auto Configurations
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2LoginAutoConfiguration
@@ -0,0 +1,34 @@
server:
port: 8080
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
# org.springframework.boot.autoconfigure: DEBUG
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
google:
client-id: your-app-client-id
client-secret: your-app-client-secret
github:
client-id: your-app-client-id
client-secret: your-app-client-secret
user-info-name-attribute-key: "name"
facebook:
client-id: your-app-client-id
client-secret: your-app-client-secret
user-info-name-attribute-key: "name"
okta:
client-id: your-app-client-id
client-secret: your-app-client-secret
authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<title>Spring Security - OAuth2 User Info</title>
<meta charset="utf-8" />
</head>
<body>
<div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">
<div style="float:left">
<span style="font-weight:bold">User: </span><span sec:authentication="name"></span>
</div>
<div style="float:none">&nbsp;</div>
<div style="float:right">
<form action="#" th:action="@{/logout}" method="post">
<input type="submit" value="Logout" />
</form>
</div>
</div>
<h1>OAuth2 User Info</h1>
<div>
<span style="font-weight:bold">Name: </span><span th:text="${userName}"></span>
</div>
<div>&nbsp;</div>
<div>
<span style="font-weight:bold">Attributes:</span>
<ul>
<li th:each="userAttribute : ${userAttributes}">
<span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>
</li>
</ul>
</div>
</body>
</html>