Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first class support for both imperative and reactive applications, it is the de-facto standard for securing Spring-based applications.
Introduction
1. Prerequisites
Spring Security requires a Java 8 or higher Runtime Environment.
As Spring Security aims to operate in a self-contained manner, you do not need to place any special configuration files in your Java Runtime Environment. In particular, you need not configure a special Java Authentication and Authorization Service (JAAS) policy file or place Spring Security into common classpath locations.
Similarly, if you use an EJB Container or Servlet Container, you need not put any special configuration files anywhere nor include Spring Security in a server classloader. All the required files are contained within your application.
This design offers maximum deployment time flexibility, as you can copy your target artifact (be it a JAR, WAR, or EAR) from one system to another and it immediately works.
2. Spring Security Community
Welcome to the Spring Security Community! This section discusses how you can make the most of our vast community.
2.1. Getting Help
If you need help with Spring Security, we are here to help. The following are some of the best ways to get help:
-
Read through this documentation.
-
Try one of our many sample applications.
-
Ask a question on https://stackoverflow.com with the
spring-security
tag. -
Report bugs and enhancement requests at https://github.com/spring-projects/spring-security/issues
2.2. Becoming Involved
We welcome your involvement in the Spring Security project. There are many ways to contribute, including answering questions on Stack Overflow, writing new code, improving existing code, assisting with documentation, developing samples or tutorials, reporting bugs, or simply making suggestions. For more information, see our Contributing documentation.
2.3. Source Code
You can find Spring Security’s source code on GitHub at https://github.com/spring-projects/spring-security/
2.4. Apache 2 License
Spring Security is Open Source software released under the Apache 2.0 license.
2.5. Social Media
You can follow @SpringSecurity and the Spring Security team on Twitter to stay up to date with the latest news. You can also follow @SpringCentral to keep up to date with the entire Spring portfolio.
3. What’s New in Spring Security 5.6
Spring Security 5.6 provides a number of new features. Below are the highlights of the release.
3.1. Servlet
-
Configuration
-
Introduced
AuthorizationManager
for method security
-
4. Getting Spring Security
This section discusses all you need to know about getting the Spring Security binaries. See Source Code for how to obtain the source code.
4.1. Release Numbering
Spring Security versions are formatted as MAJOR.MINOR.PATCH such that:
-
MAJOR versions may contain breaking changes. Typically, these are done to provide improved security to match modern security practices.
-
MINOR versions contain enhancements but are considered passive updates
-
PATCH level should be perfectly compatible, forwards and backwards, with the possible exception of changes that fix bugs.
4.2. Usage with Maven
As most open source projects, Spring Security deploys its dependencies as Maven artifacts. The topics in this section provide detail on how to consume Spring Security when using Maven.
4.2.1. Spring Boot with Maven
Spring Boot provides a spring-boot-starter-security
starter that aggregates Spring Security-related dependencies together.
The simplest and preferred way to use the starter is to use Spring Initializr by using an IDE integration (Eclipse, IntelliJ, NetBeans) or through https://start.spring.io.
Alternatively, you can manually add the starter, as the following example shows:
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
Since Spring Boot provides a Maven BOM to manage dependency versions, you do not need to specify a version. If you wish to override the Spring Security version, you may do so by providing a Maven property, as the following example shows:
<properties>
<!-- ... -->
<spring-security.version>5.6.0-M1</spring-security.version>
</dependencies>
Since Spring Security makes breaking changes only in major releases, it is safe to use a newer version of Spring Security with Spring Boot. However, at times, you may need to update the version of Spring Framework as well. You can do so by adding a Maven property, as the following example shows:
<properties>
<!-- ... -->
<spring.version>5.3.9</spring.version>
</dependencies>
If you use additional features (such as LDAP, OpenID, and others), you need to also include the appropriate Project Modules and Dependencies.
4.2.2. Maven Without Spring Boot
When you use Spring Security without Spring Boot, the preferred way is to use Spring Security’s BOM to ensure a consistent version of Spring Security is used throughout the entire project. The following example shows how to do so:
<dependencyManagement>
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>{spring-security-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
A minimal Spring Security Maven set of dependencies typically looks like the following:
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
</dependencies>
If you use additional features (such as LDAP, OpenID, and others), you need to also include the appropriate Project Modules and Dependencies.
Spring Security builds against Spring Framework 5.3.9 but should generally work with any newer version of Spring Framework 5.x.
Many users are likely to run afoul of the fact that Spring Security’s transitive dependencies resolve Spring Framework 5.3.9, which can cause strange classpath problems.
The easiest way to resolve this is to use the spring-framework-bom
within the <dependencyManagement>
section of your pom.xml
as the following example shows:
<dependencyManagement>
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.3.9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
The preceding example ensures that all the transitive dependencies of Spring Security use the Spring 5.3.9 modules.
This approach uses Maven’s “bill of materials” (BOM) concept and is only available in Maven 2.0.9+. For additional details about how dependencies are resolved, see Maven’s Introduction to the Dependency Mechanism documentation. |
4.2.3. Maven Repositories
All GA releases (that is, versions ending in .RELEASE) are deployed to Maven Central, so no additional Maven repositories need to be declared in your pom.
If you use a SNAPSHOT version, you need to ensure that you have the Spring Snapshot repository defined, as the following example shows:
<repositories>
<!-- ... possibly other repository elements ... -->
<repository>
<id>spring-snapshot</id>
<name>Spring Snapshot Repository</name>
<url>https://repo.spring.io/snapshot</url>
</repository>
</repositories>
If you use a milestone or release candidate version, you need to ensure that you have the Spring Milestone repository defined, as the following example shows:
<repositories>
<!-- ... possibly other repository elements ... -->
<repository>
<id>spring-milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
4.3. Gradle
As most open source projects, Spring Security deploys its dependencies as Maven artifacts, which allows for first-class Gradle support. The following topics provide detail on how to consume Spring Security when using Gradle.
4.3.1. Spring Boot with Gradle
Spring Boot provides a spring-boot-starter-security
starter that aggregates Spring Security related dependencies together.
The simplest and preferred method to use the starter is to use Spring Initializr by using an IDE integration (Eclipse, IntelliJ, NetBeans) or through https://start.spring.io.
Alternatively, you can manually add the starter, as the following example shows:
dependencies {
compile "org.springframework.boot:spring-boot-starter-security"
}
Since Spring Boot provides a Maven BOM to manage dependency versions, you need not specify a version. If you wish to override the Spring Security version, you may do so by providing a Gradle property, as the following example shows:
ext['spring-security.version']='5.6.0-M1'
Since Spring Security makes breaking changes only in major releases, it is safe to use a newer version of Spring Security with Spring Boot. However, at times, you may need to update the version of Spring Framework as well. You can do so by adding a Gradle property, as the following example shows:
ext['spring.version']='5.3.9'
If you use additional features (such as LDAP, OpenID, and others), you need to also include the appropriate Project Modules and Dependencies.
4.3.2. Gradle Without Spring Boot
When you use Spring Security without Spring Boot, the preferred way is to use Spring Security’s BOM to ensure a consistent version of Spring Security is used throughout the entire project. You can do so by using the Dependency Management Plugin, as the following example shows:
plugins {
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}
dependencyManagement {
imports {
mavenBom 'org.springframework.security:spring-security-bom:5.6.0-M1'
}
}
A minimal Spring Security Maven set of dependencies typically looks like the following:
dependencies {
compile "org.springframework.security:spring-security-web"
compile "org.springframework.security:spring-security-config"
}
If you use additional features (such as LDAP, OpenID, and others), you need to also include the appropriate Project Modules and Dependencies.
Spring Security builds against Spring Framework 5.3.9 but should generally work with any newer version of Spring Framework 5.x.
Many users are likely to run afoul of the fact that Spring Security’s transitive dependencies resolve Spring Framework 5.3.9, which can cause strange classpath problems.
The easiest way to resolve this is to use the spring-framework-bom
within your <dependencyManagement>
section of your pom.xml
.
You can do so by using the Dependency Management Plugin, as the following example shows:
plugins {
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}
dependencyManagement {
imports {
mavenBom 'org.springframework:spring-framework-bom:5.3.9'
}
}
The preceding example ensures that all the transitive dependencies of Spring Security use the Spring 5.3.9 modules.
4.3.3. Gradle Repositories
All GA releases (that is, versions ending in .RELEASE) are deployed to Maven Central, so using the mavenCentral() repository is sufficient for GA releases. The following example shows how to do so:
repositories {
mavenCentral()
}
If you use a SNAPSHOT version, you need to ensure you have the Spring Snapshot repository defined, as the following example shows:
repositories {
maven { url 'https://repo.spring.io/snapshot' }
}
If you use a milestone or release candidate version, you need to ensure that you have the Spring Milestone repository defined, as the following example shows:
repositories {
maven { url 'https://repo.spring.io/milestone' }
}
5. Features
Spring Security provides comprehensive support for authentication, authorization, and protection against common exploits. It also provides integration with other libraries to simplify its usage.
5.1. Authentication
Spring Security provides comprehensive support for authentication. Authentication is how we verify the identity of who is trying to access a particular resource. A common way to authenticate users is by requiring the user to enter a username and password. Once authentication is performed we know the identity and can perform authorization.
5.1.1. Authentication Support
Spring Security provides built in support for authenticating users. Refer to the sections on authentication for Servlet and WebFlux for details on what is supported for each stack.
5.1.2. Password Storage
Spring Security’s PasswordEncoder
interface is used to perform a one way transformation of a password to allow the password to be stored securely.
Given PasswordEncoder
is a one way transformation, it is not intended when the password transformation needs to be two way (i.e. storing credentials used to authenticate to a database).
Typically PasswordEncoder
is used for storing a password that needs to be compared to a user provided password at the time of authentication.
Password Storage History
Throughout the years the standard mechanism for storing passwords has evolved. In the beginning passwords were stored in plain text. The passwords were assumed to be safe because the data store the passwords were saved in required credentials to access it. However, malicious users were able to find ways to get large "data dumps" of usernames and passwords using attacks like SQL Injection. As more and more user credentials became public security experts realized we needed to do more to protect users' passwords.
Developers were then encouraged to store passwords after running them through a one way hash such as SHA-256. When a user tried to authenticate, the hashed password would be compared to the hash of the password that they typed. This meant that the system only needed to store the one way hash of the password. If a breach occurred, then only the one way hashes of the passwords were exposed. Since the hashes were one way and it was computationally difficult to guess the passwords given the hash, it would not be worth the effort to figure out each password in the system. To defeat this new system malicious users decided to create lookup tables known as Rainbow Tables. Rather than doing the work of guessing each password every time, they computed the password once and stored it in a lookup table.
To mitigate the effectiveness of Rainbow Tables, developers were encouraged to use salted passwords. Instead of using just the password as input to the hash function, random bytes (known as salt) would be generated for every users' password. The salt and the user’s password would be ran through the hash function which produced a unique hash. The salt would be stored alongside the user’s password in clear text. Then when a user tried to authenticate, the hashed password would be compared to the hash of the stored salt and the password that they typed. The unique salt meant that Rainbow Tables were no longer effective because the hash was different for every salt and password combination.
In modern times we realize that cryptographic hashes (like SHA-256) are no longer secure. The reason is that with modern hardware we can perform billions of hash calculations a second. This means that we can crack each password individually with ease.
Developers are now encouraged to leverage adaptive one-way functions to store a password. Validation of passwords with adaptive one-way functions are intentionally resource (i.e. CPU, memory, etc) intensive. An adaptive one-way function allows configuring a "work factor" which can grow as hardware gets better. It is recommended that the "work factor" be tuned to take about 1 second to verify a password on your system. This trade off is to make it difficult for attackers to crack the password, but not so costly it puts excessive burden on your own system. Spring Security has attempted to provide a good starting point for the "work factor", but users are encouraged to customize the "work factor" for their own system since the performance will vary drastically from system to system. Examples of adaptive one-way functions that should be used include bcrypt, PBKDF2, scrypt, and argon2.
Because adaptive one-way functions are intentionally resource intensive, validating a username and password for every request will degrade performance of an application significantly. There is nothing Spring Security (or any other library) can do to speed up the validation of the password since security is gained by making the validation resource intensive. Users are encouraged to exchange the long term credentials (i.e. username and password) for a short term credential (i.e. session, OAuth Token, etc). The short term credential can be validated quickly without any loss in security.
DelegatingPasswordEncoder
Prior to Spring Security 5.0 the default PasswordEncoder
was NoOpPasswordEncoder
which required plain text passwords.
Based upon the Password History section you might expect that the default PasswordEncoder
is now something like BCryptPasswordEncoder
.
However, this ignores three real world problems:
-
There are many applications using old password encodings that cannot easily migrate
-
The best practice for password storage will change again.
-
As a framework Spring Security cannot make breaking changes frequently
Instead Spring Security introduces DelegatingPasswordEncoder
which solves all of the problems by:
-
Ensuring that passwords are encoded using the current password storage recommendations
-
Allowing for validating passwords in modern and legacy formats
-
Allowing for upgrading the encoding in the future
You can easily construct an instance of DelegatingPasswordEncoder
using PasswordEncoderFactories
.
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
Alternatively, you may create your own custom instance. For example:
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);
val idForEncode = "bcrypt"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = BCryptPasswordEncoder()
encoders["noop"] = NoOpPasswordEncoder.getInstance()
encoders["pbkdf2"] = Pbkdf2PasswordEncoder()
encoders["scrypt"] = SCryptPasswordEncoder()
encoders["sha256"] = StandardPasswordEncoder()
val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)
Password Storage Format
The general format for a password is:
{id}encodedPassword
Such that id
is an identifier used to look up which PasswordEncoder
should be used and encodedPassword
is the original encoded password for the selected PasswordEncoder
.
The id
must be at the beginning of the password, start with {
and end with }
.
If the id
cannot be found, the id
will be null.
For example, the following might be a list of passwords encoded using different id
.
All of the original passwords are "password".
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG (1)
{noop}password (2)
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc (3)
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= (4)
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 (5)
1 | The first password would have a PasswordEncoder id of bcrypt and encodedPassword of $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG .
When matching it would delegate to BCryptPasswordEncoder |
2 | The second password would have a PasswordEncoder id of noop and encodedPassword of password .
When matching it would delegate to NoOpPasswordEncoder |
3 | The third password would have a PasswordEncoder id of pbkdf2 and encodedPassword of 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc .
When matching it would delegate to Pbkdf2PasswordEncoder |
4 | The fourth password would have a PasswordEncoder id of scrypt and encodedPassword of $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
When matching it would delegate to SCryptPasswordEncoder |
5 | The final password would have a PasswordEncoder id of sha256 and encodedPassword of 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 .
When matching it would delegate to StandardPasswordEncoder |
Some users might be concerned that the storage format is provided for a potential hacker.
This is not a concern because the storage of the password does not rely on the algorithm being a secret.
Additionally, most formats are easy for an attacker to figure out without the prefix.
For example, BCrypt passwords often start with |
Password Encoding
The idForEncode
passed into the constructor determines which PasswordEncoder
will be used for encoding passwords.
In the DelegatingPasswordEncoder
we constructed above, that means that the result of encoding password
would be delegated to BCryptPasswordEncoder
and be prefixed with {bcrypt}
.
The end result would look like:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
Password Matching
Matching is done based upon the {id}
and the mapping of the id
to the PasswordEncoder
provided in the constructor.
Our example in Password Storage Format provides a working example of how this is done.
By default, the result of invoking matches(CharSequence, String)
with a password and an id
that is not mapped (including a null id) will result in an IllegalArgumentException
.
This behavior can be customized using DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)
.
By using the id
we can match on any password encoding, but encode passwords using the most modern password encoding.
This is important, because unlike encryption, password hashes are designed so that there is no simple way to recover the plaintext.
Since there is no way to recover the plaintext, it makes it difficult to migrate the passwords.
While it is simple for users to migrate NoOpPasswordEncoder
, we chose to include it by default to make it simple for the getting started experience.
Getting Started Experience
If you are putting together a demo or a sample, it is a bit cumbersome to take time to hash the passwords of your users. There are convenience mechanisms to make this easier, but this is still not intended for production.
User user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build()
println(user.password)
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
If you are creating multiple users, you can also reuse the builder.
UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
.username("user")
.password("password")
.roles("USER")
.build();
User admin = users
.username("admin")
.password("password")
.roles("USER","ADMIN")
.build();
val users = User.withDefaultPasswordEncoder()
val user = users
.username("user")
.password("password")
.roles("USER")
.build()
val admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build()
This does hash the password that is stored, but the passwords are still exposed in memory and in the compiled source code. Therefore, it is still not considered secure for a production environment. For production, you should hash your passwords externally.
Encode with Spring Boot CLI
The easiest way to properly encode your password is to use the Spring Boot CLI.
For example, the following will encode the password of password
for use with DelegatingPasswordEncoder:
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6
Troubleshooting
The following error occurs when one of the passwords that are stored has no id as described in Password Storage Format.
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233) at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
The easiest way to resolve the error is to switch to explicitly provide the PasswordEncoder
that you passwords are encoded with.
The easiest way to resolve it is to figure out how your passwords are currently being stored and explicitly provide the correct PasswordEncoder
.
If you are migrating from Spring Security 4.2.x you can revert to the previous behavior by exposing a NoOpPasswordEncoder
bean.
Alternatively, you can prefix all of your passwords with the correct id and continue to use DelegatingPasswordEncoder
.
For example, if you are using BCrypt, you would migrate your password from something like:
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
to
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
For a complete listing of the mappings refer to the Javadoc on PasswordEncoderFactories.
BCryptPasswordEncoder
The BCryptPasswordEncoder
implementation uses the widely supported bcrypt algorithm to hash the passwords.
In order to make it more resistent to password cracking, bcrypt is deliberately slow.
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system.
The default implementation of BCryptPasswordEncoder
uses strength 10 as mentioned in the Javadoc of BCryptPasswordEncoder. You are encouraged to
tune and test the strength parameter on your own system so that it takes roughly 1 second to verify a password.
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with strength 16
val encoder = BCryptPasswordEncoder(16)
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
Argon2PasswordEncoder
The Argon2PasswordEncoder
implementation uses the Argon2 algorithm to hash the passwords.
Argon2 is the winner of the Password Hashing Competition.
In order to defeat password cracking on custom hardware, Argon2 is a deliberately slow algorithm that requires large amounts of memory.
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system.
The current implementation of the Argon2PasswordEncoder
requires BouncyCastle.
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = Argon2PasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
Pbkdf2PasswordEncoder
The Pbkdf2PasswordEncoder
implementation uses the PBKDF2 algorithm to hash the passwords.
In order to defeat password cracking PBKDF2 is a deliberately slow algorithm.
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system.
This algorithm is a good choice when FIPS certification is required.
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = Pbkdf2PasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
SCryptPasswordEncoder
The SCryptPasswordEncoder
implementation uses scrypt algorithm to hash the passwords.
In order to defeat password cracking on custom hardware scrypt is a deliberately slow algorithm that requires large amounts of memory.
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system.
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = SCryptPasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
Other PasswordEncoders
There are a significant number of other PasswordEncoder
implementations that exist entirely for backward compatibility.
They are all deprecated to indicate that they are no longer considered secure.
However, there are no plans to remove them since it is difficult to migrate existing legacy systems.
Password Storage Configuration
Spring Security uses DelegatingPasswordEncoder by default.
However, this can be customized by exposing a PasswordEncoder
as a Spring bean.
If you are migrating from Spring Security 4.2.x you can revert to the previous behavior by exposing a NoOpPasswordEncoder
bean.
Reverting to |
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
<b:bean id="passwordEncoder"
class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance();
}
XML Configuration requires the |
Change Password Configuration
Most applications that allow a user to specify a password also require a feature for updating that password.
A Well-Know URL for Changing Passwords indicates a mechanism by which password managers can discover the password update endpoint for a given application.
You can configure Spring Security to provide this discovery endpoint.
For example, if the change password endpoint in your application is /change-password
, then you can configure Spring Security like so:
http
.passwordManagement(Customizer.withDefaults())
<sec:password-management/>
http {
passwordManagement { }
}
Then, when a password manager navigates to /.well-known/change-password
then Spring Security will redirect your endpoint, /change-password
.
Or, if your endpoint is something other than /change-password
, you can also specify that like so:
http
.passwordManagement((management) -> management
.changePasswordPage("/update-password")
)
<sec:password-management change-password-page="/update-password"/>
http {
passwordManagement {
changePasswordPage = "/update-password"
}
}
With the above configuration, when a password manager navigates to /.well-known/change-password
, then Spring Security will redirect to /update-password
.
5.2. Protection Against Exploits
Spring Security provides protection against common exploits. Whenever possible, the protection is enabled by default. Below you will find high level description of the various exploits that Spring Security protects against.
5.2.1. Cross Site Request Forgery (CSRF)
Spring provides comprehensive support for protecting against Cross Site Request Forgery (CSRF) attacks. In the following sections we will explore:
What is a CSRF Attack?
The best way to understand a CSRF attack is by taking a look at a concrete example.
Assume that your bank’s website provides a form that allows transferring money from the currently logged in user to another bank account. For example, the transfer form might look like:
<form method="post"
action="/transfer">
<input type="text"
name="amount"/>
<input type="text"
name="routingNumber"/>
<input type="text"
name="account"/>
<input type="submit"
value="Transfer"/>
</form>
The corresponding HTTP request might look like:
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876
Now pretend you authenticate to your bank’s website and then, without logging out, visit an evil website. The evil website contains an HTML page with the following form:
<form method="post"
action="https://bank.example.com/transfer">
<input type="hidden"
name="amount"
value="100.00"/>
<input type="hidden"
name="routingNumber"
value="evilsRoutingNumber"/>
<input type="hidden"
name="account"
value="evilsAccountNumber"/>
<input type="submit"
value="Win Money!"/>
</form>
You like to win money, so you click on the submit button. In the process, you have unintentionally transferred $100 to a malicious user. This happens because, while the evil website cannot see your cookies, the cookies associated with your bank are still sent along with the request.
Worst yet, this whole process could have been automated using JavaScript. This means you didn’t even need to click on the button. Furthermore, it could just as easily happen when visiting an honest site that is a victim of a XSS attack. So how do we protect our users from such attacks?
Protecting Against CSRF Attacks
The reason that a CSRF attack is possible is that the HTTP request from the victim’s website and the request from the attacker’s website are exactly the same. This means there is no way to reject requests coming from the evil website and allow requests coming from the bank’s website. To protect against CSRF attacks we need to ensure there is something in the request that the evil site is unable to provide so we can differentiate the two requests.
Spring provides two mechanisms to protect against CSRF attacks:
-
Specifying the SameSite Attribute on your session cookie
Both protections require that Safe Methods Must be Idempotent |
Safe Methods Must be Idempotent
In order for either protection against CSRF to work, the application must ensure that "safe" HTTP methods are idempotent.
This means that requests with the HTTP method GET
, HEAD
, OPTIONS
, and TRACE
should not change the state of the application.
Synchronizer Token Pattern
The predominant and most comprehensive way to protect against CSRF attacks is to use the Synchronizer Token Pattern. This solution is to ensure that each HTTP request requires, in addition to our session cookie, a secure random generated value called a CSRF token must be present in the HTTP request.
When an HTTP request is submitted, the server must look up the expected CSRF token and compare it against the actual CSRF token in the HTTP request. If the values do not match, the HTTP request should be rejected.
The key to this working is that the actual CSRF token should be in a part of the HTTP request that is not automatically included by the browser. For example, requiring the actual CSRF token in an HTTP parameter or an HTTP header will protect against CSRF attacks. Requiring the actual CSRF token in a cookie does not work because cookies are automatically included in the HTTP request by the browser.
We can relax the expectations to only require the actual CSRF token for each HTTP request that updates state of the application. For that to work, our application must ensure that safe HTTP methods are idempotent. This improves usability since we want to allow linking to our website using links from external sites. Additionally, we do not want to include the random token in HTTP GET as this can cause the tokens to be leaked.
Let’s take a look at how our example would change when using the Synchronizer Token Pattern.
Assume the actual CSRF token is required to be in an HTTP parameter named _csrf
.
Our application’s transfer form would look like:
<form method="post"
action="/transfer">
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<input type="text"
name="amount"/>
<input type="text"
name="routingNumber"/>
<input type="hidden"
name="account"/>
<input type="submit"
value="Transfer"/>
</form>
The form now contains a hidden input with the value of the CSRF token. External sites cannot read the CSRF token since the same origin policy ensures the evil site cannot read the response.
The corresponding HTTP request to transfer money would look like this:
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
You will notice that the HTTP request now contains the _csrf
parameter with a secure random value.
The evil website will not be able to provide the correct value for the _csrf
parameter (which must be explicitly provided on the evil website) and the transfer will fail when the server compares the actual CSRF token to the expected CSRF token.
SameSite Attribute
An emerging way to protect against CSRF Attacks is to specify the SameSite Attribute on cookies.
A server can specify the SameSite
attribute when setting a cookie to indicate that the cookie should not be sent when coming from external sites.
Spring Security does not directly control the creation of the session cookie, so it does not provide support for the SameSite attribute.
Spring Session provides support for the |
An example, HTTP response header with the SameSite
attribute might look like:
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
Valid values for the SameSite
attribute are:
-
Strict
- when specified any request coming from the same-site will include the cookie. Otherwise, the cookie will not be included in the HTTP request. -
Lax
- when specified cookies will be sent when coming from the same-site or when the request comes from top-level navigations and the method is idempotent. Otherwise, the cookie will not be included in the HTTP request.
Let’s take a look at how our example could be protected using the SameSite
attribute.
The bank application can protect against CSRF by specifying the SameSite
attribute on the session cookie.
With the SameSite
attribute set on our session cookie, the browser will continue to send the JSESSIONID
cookie with requests coming from the banking website.
However, the browser will no longer send the JSESSIONID
cookie with a transfer request coming from the evil website.
Since the session is no longer present in the transfer request coming from the evil website, the application is protected from the CSRF attack.
There are some important considerations that one should be aware about when using SameSite
attribute to protect against CSRF attacks.
Setting the SameSite
attribute to Strict
provides a stronger defense but can confuse users.
Consider a user that stays logged into a social media site hosted at https://social.example.com.
The user receives an email at https://email.example.org that includes a link to the social media site.
If the user clicks on the link, they would rightfully expect to be authenticated to the social media site.
However, if the SameSite
attribute is Strict
the cookie would not be sent and so the user would not be authenticated.
We could improve the protection and usability of |
Another obvious consideration is that in order for the SameSite
attribute to protect users, the browser must support the SameSite
attribute.
Most modern browsers do support the SameSite attribute.
However, older browsers that are still in use may not.
For this reason, it is generally recommended to use the SameSite
attribute as a defense in depth rather than the sole protection against CSRF attacks.
When to use CSRF protection
When should you use CSRF protection? Our recommendation is to use CSRF protection for any request that could be processed by a browser by normal users. If you are only creating a service that is used by non-browser clients, you will likely want to disable CSRF protection.
CSRF protection and JSON
A common question is "do I need to protect JSON requests made by javascript?" The short answer is, it depends. However, you must be very careful as there are CSRF exploits that can impact JSON requests. For example, a malicious user can create a CSRF with JSON using the following form:
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
value="Win Money!"/>
</form>
This will produce the following JSON structure
{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}
If an application were not validating the Content-Type, then it would be exposed to this exploit.
Depending on the setup, a Spring MVC application that validates the Content-Type could still be exploited by updating the URL suffix to end with .json
as shown below:
<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
value="Win Money!"/>
</form>
CSRF and Stateless Browser Applications
What if my application is stateless? That doesn’t necessarily mean you are protected. In fact, if a user does not need to perform any actions in the web browser for a given request, they are likely still vulnerable to CSRF attacks.
For example, consider an application that uses a custom cookie that contains all the state within it for authentication instead of the JSESSIONID. When the CSRF attack is made the custom cookie will be sent with the request in the same manner that the JSESSIONID cookie was sent in our previous example. This application will be vulnerable to CSRF attacks.
Applications that use basic authentication are also vulnerable to CSRF attacks. The application is vulnerable since the browser will automatically include the username and password in any requests in the same manner that the JSESSIONID cookie was sent in our previous example.
CSRF Considerations
There are a few special considerations to consider when implementing protection against CSRF attacks.
Logging In
In order to protect against forging log in requests the log in HTTP request should be protected against CSRF attacks. Protecting against forging log in requests is necessary so that a malicious user cannot read a victim’s sensitive information. The attack is performed as follows:
-
A malicious user performs a CSRF log in using the malicious user’s credentials. The victim is now authenticated as the malicious user.
-
The malicious user then tricks the victim to visit the compromised website and enter sensitive information
-
The information is associated to the malicious user’s account so the malicious user can log in with their own credentials and view the vicitim’s sensitive information
A possible complication to ensuring log in HTTP requests are protected against CSRF attacks is that the user might experience a session timeout that causes the request to be rejected. A session timeout is surprising to users who do not expect to need to have a session in order to log in. For more information refer to CSRF and Session Timeouts.
Logging Out
In order to protect against forging log out requests, the log out HTTP request should be protected against CSRF attacks. Protecting against forging log out requests is necessary so a malicious user cannot read a victim’s sensitive information. For details on the attack refer to this blog post.
A possible complication to ensuring log out HTTP requests are protected against CSRF attacks is that the user might experience a session timeout that causes the request to be rejected. A session timeout is surprising to users who do not expect to need to have a session in order to log out. For more information refer to CSRF and Session Timeouts.
CSRF and Session Timeouts
More often than not, the expected CSRF token is stored in the session. This means that as soon as the session expires the server will not find an expected CSRF token and reject the HTTP request. There are a number of options to solve timeouts each of which come with trade offs.
-
The best way to mitigate the timeout is by using JavaScript to request a CSRF token on form submission. The form is then updated with the CSRF token and submitted.
-
Another option is to have some JavaScript that lets the user know their session is about to expire. The user can click a button to continue and refresh the session.
-
Finally, the expected CSRF token could be stored in a cookie. This allows the expected CSRF token to outlive the session.
One might ask why the expected CSRF token isn’t stored in a cookie by default. This is because there are known exploits in which headers (for example, to specify the cookies) can be set by another domain. This is the same reason Ruby on Rails no longer skips CSRF checks when the header X-Requested-With is present. See this webappsec.org thread for details on how to perform the exploit. Another disadvantage is that by removing the state (that is, the timeout), you lose the ability to forcibly invalidate the token if it is compromised.
Multipart (file upload)
Protecting multipart requests (file uploads) from CSRF attacks causes a chicken and the egg problem. In order to prevent a CSRF attack from occurring, the body of the HTTP request must be read to obtain actual CSRF token. However, reading the body means that the file will be uploaded which means an external site can upload a file.
There are two options to using CSRF protection with multipart/form-data. Each option has its trade-offs.
Before you integrate Spring Security’s CSRF protection with multipart file upload, ensure that you can upload without the CSRF protection first. More information about using multipart forms with Spring can be found within the 1.1.11. Multipart Resolver section of the Spring reference and the MultipartFilter javadoc. |
The first option is to include the actual CSRF token in the body of the request. By placing the CSRF token in the body, the body will be read before authorization is performed. This means that anyone can place temporary files on your server. However, only authorized users will be able to submit a File that is processed by your application. In general, this is the recommended approach because the temporary file upload should have a negligible impact on most servers.
If allowing unauthorized users to upload temporary files is not acceptable, an alternative is to include the expected CSRF token as a query parameter in the action attribute of the form. The disadvantage to this approach is that query parameters can be leaked. More generally, it is considered best practice to place sensitive data within the body or headers to ensure it is not leaked. Additional information can be found in RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s.
In some applications a form parameter can be used to override the HTTP method.
For example, the form below could be used to treat the HTTP method as a delete
rather than a post
.
<form action="/process"
method="post">
<!-- ... -->
<input type="hidden"
name="_method"
value="delete"/>
</form>
Overriding the HTTP method occurs in a filter.
That filter must be placed before Spring Security’s support.
Note that overriding only happens on a post
, so this is actually unlikely to cause any real problems.
However, it is still best practice to ensure it is placed before Spring Security’s filters.
5.2.2. Security HTTP Response Headers
There are many HTTP response headers that can be used to increase the security of web applications. This section is dedicated to the various HTTP response headers that Spring Security provides explicit support for. If necessary, Spring Security can also be configured to provide custom headers.
Default Security Headers
Spring Security provides a default set of security related HTTP response headers to provide secure defaults.
The default for Spring Security is to include the following headers:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Strict-Transport-Security is only added on HTTPS requests |
If the defaults do not meet your needs, you can easily remove, modify, or add headers from these defaults. For additional details on each of these headers, refer to the corresponding sections:
Cache Control
Spring Security’s default is to disable caching to protect user’s content.
If a user authenticates to view sensitive information and then logs out, we don’t want a malicious user to be able to click the back button to view the sensitive information. The cache control headers that are sent by default are:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
In order to be secure by default, Spring Security adds these headers by default. However, if your application provides its own cache control headers Spring Security will back out of the way. This allows for applications to ensure that static resources like CSS and JavaScript can be cached.
Content Type Options
Historically browsers, including Internet Explorer, would try to guess the content type of a request using content sniffing. This allowed browsers to improve the user experience by guessing the content type on resources that had not specified the content type. For example, if a browser encountered a JavaScript file that did not have the content type specified, it would be able to guess the content type and then run it.
There are many additional things one should do (i.e. only display the document in a distinct domain, ensure Content-Type header is set, sanitize the document, etc) when allowing content to be uploaded. However, these measures are out of the scope of what Spring Security provides. It is also important to point out when disabling content sniffing, you must specify the content type in order for things to work properly. |
The problem with content sniffing is that this allowed malicious users to use polyglots (i.e. a file that is valid as multiple content types) to perform XSS attacks. For example, some sites may allow users to submit a valid postscript document to a website and view it. A malicious user might create a postscript document that is also a valid JavaScript file and perform a XSS attack with it.
Spring Security disables content sniffing by default by adding the following header to HTTP responses:
X-Content-Type-Options: nosniff
HTTP Strict Transport Security (HSTS)
When you type in your bank’s website, do you enter mybank.example.com or do you enter https://mybank.example.com? If you omit the https protocol, you are potentially vulnerable to Man in the Middle attacks. Even if the website performs a redirect to https://mybank.example.com a malicious user could intercept the initial HTTP request and manipulate the response (e.g. redirect to https://mibank.example.com and steal their credentials).
Many users omit the https protocol and this is why HTTP Strict Transport Security (HSTS) was created. Once mybank.example.com is added as a HSTS host, a browser can know ahead of time that any request to mybank.example.com should be interpreted as https://mybank.example.com. This greatly reduces the possibility of a Man in the Middle attack occurring.
In accordance with RFC6797, the HSTS header is only injected into HTTPS responses. In order for the browser to acknowledge the header, the browser must first trust the CA that signed the SSL certificate used to make the connection (not just the SSL certificate). |
One way for a site to be marked as a HSTS host is to have the host preloaded into the browser.
Another is to add the Strict-Transport-Security
header to the response.
For example, Spring Security’s default behavior is to add the following header which instructs the browser to treat the domain as an HSTS host for a year (there are approximately 31536000 seconds in a year):
Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload
The optional includeSubDomains
directive instructs the browser that subdomains (e.g. secure.mybank.example.com) should also be treated as an HSTS domain.
The optional preload
directive instructs the browser that domain should be preloaded in browser as HSTS domain.
For more details on HSTS preload please see https://hstspreload.org.
HTTP Public Key Pinning (HPKP)
In order to remain passive Spring Security still provides support for HPKP in servlet environments, but for the reasons listed above HPKP is no longer recommended by the security team. |
HTTP Public Key Pinning (HPKP) specifies to a web client which public key to use with certain web server to prevent Man in the Middle (MITM) attacks with forged certificates. When used correctly, HPKP could add additional layers of protection against compromised certificates. However, due to the complexity of HPKP many experts no longer recommend using it and Chrome has even removed support for it.
For additional details around why HPKP is no longer recommended read Is HTTP Public Key Pinning Dead? and I’m giving up on HPKP.
X-Frame-Options
Allowing your website to be added to a frame can be a security issue. For example, using clever CSS styling users could be tricked into clicking on something that they were not intending. For example, a user that is logged into their bank might click a button that grants access to other users. This sort of attack is known as Clickjacking.
Another modern approach to dealing with clickjacking is to use Content Security Policy (CSP). |
There are a number ways to mitigate clickjacking attacks. For example, to protect legacy browsers from clickjacking attacks you can use frame breaking code. While not perfect, the frame breaking code is the best you can do for the legacy browsers.
A more modern approach to address clickjacking is to use X-Frame-Options header. By default Spring Security disables rendering pages within an iframe using with the following header:
X-Frame-Options: DENY
X-XSS-Protection
Some browsers have built in support for filtering out reflected XSS attacks. This is by no means foolproof, but does assist in XSS protection.
The filtering is typically enabled by default, so adding the header typically just ensures it is enabled and instructs the browser what to do when a XSS attack is detected. For example, the filter might try to change the content in the least invasive way to still render everything. At times, this type of replacement can become a XSS vulnerability in itself. Instead, it is best to block the content rather than attempt to fix it. By default Spring Security blocks the content using the following header:
X-XSS-Protection: 1; mode=block
Content Security Policy (CSP)
Content Security Policy (CSP) is a mechanism that web applications can leverage to mitigate content injection vulnerabilities, such as cross-site scripting (XSS). CSP is a declarative policy that provides a facility for web application authors to declare and ultimately inform the client (user-agent) about the sources from which the web application expects to load resources.
Content Security Policy is not intended to solve all content injection vulnerabilities. Instead, CSP can be leveraged to help reduce the harm caused by content injection attacks. As a first line of defense, web application authors should validate their input and encode their output. |
A web application may employ the use of CSP by including one of the following HTTP headers in the response:
-
Content-Security-Policy
-
Content-Security-Policy-Report-Only
Each of these headers are used as a mechanism to deliver a security policy to the client. A security policy contains a set of security policy directives, each responsible for declaring the restrictions for a particular resource representation.
For example, a web application can declare that it expects to load scripts from specific, trusted sources, by including the following header in the response:
Content-Security-Policy: script-src https://trustedscripts.example.com
An attempt to load a script from another source other than what is declared in the script-src
directive will be blocked by the user-agent.
Additionally, if the report-uri directive is declared in the security policy, then the violation will be reported by the user-agent to the declared URL.
For example, if a web application violates the declared security policy, the following response header will instruct the user-agent to send violation reports to the URL specified in the policy’s report-uri
directive.
Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/
Violation reports are standard JSON structures that can be captured either by the web application’s own API or by a publicly hosted CSP violation reporting service, such as, https://report-uri.com/.
The Content-Security-Policy-Report-Only
header provides the capability for web application authors and administrators to monitor security policies, rather than enforce them.
This header is typically used when experimenting and/or developing security policies for a site.
When a policy is deemed effective, it can be enforced by using the Content-Security-Policy
header field instead.
Given the following response header, the policy declares that scripts may be loaded from one of two possible sources.
Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/
If the site violates this policy, by attempting to load a script from evil.com, the user-agent will send a violation report to the declared URL specified by the report-uri directive, but still allow the violating resource to load nevertheless.
Applying Content Security Policy to a web application is often a non-trivial undertaking. The following resources may provide further assistance in developing effective security policies for your site.
Referrer Policy
Referrer Policy is a mechanism that web applications can leverage to manage the referrer field, which contains the last page the user was on.
Spring Security’s approach is to use Referrer Policy header, which provides different policies:
Referrer-Policy: same-origin
The Referrer-Policy response header instructs the browser to let the destination knows the source where the user was previously.
Feature Policy
Feature Policy is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser.
Feature-Policy: geolocation 'self'
With Feature Policy, developers can opt-in to a set of "policies" for the browser to enforce on specific features used throughout your site. These policies restrict what APIs the site can access or modify the browser’s default behavior for certain features.
Permissions Policy
Permissions Policy is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser.
Permissions-Policy: geolocation=(self)
With Permissions Policy, developers can opt-in to a set of "policies" for the browser to enforce on specific features used throughout your site. These policies restrict what APIs the site can access or modify the browser’s default behavior for certain features.
Clear Site Data
Clear Site Data is a mechanism by which any browser-side data - cookies, local storage, and the like - can be removed when an HTTP response contains this header:
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"
This is a nice clean-up action to perform on logout.
Custom Headers
Refer to the relevant sections to see how to configure both servlet based applications. |
Spring Security has mechanisms to make it convenient to add the more common security headers to your application. However, it also provides hooks to enable adding custom headers.
5.2.3. HTTP
All HTTP based communication, including static resources, should be protected using TLS.
As a framework, Spring Security does not handle HTTP connections and thus does not provide support for HTTPS directly. However, it does provide a number of features that help with HTTPS usage.
Strict Transport Security
Spring Security provides support for Strict Transport Security and enables it by default.
Proxy Server Configuration
When using a proxy server it is important to ensure that you have configured your application properly. For example, many applications will have a load balancer that responds to request for https://example.com/ by forwarding the request to an application server at https://192.168.1:8080 Without proper configuration, the application server will not know that the load balancer exists and treat the request as though https://192.168.1:8080 was requested by the client.
To fix this you can use RFC 7239 to specify that a load balancer is being used. To make the application aware of this, you need to either configure your application server aware of the X-Forwarded headers. For example Tomcat uses the RemoteIpValve and Jetty uses ForwardedRequestCustomizer. Alternatively, Spring users can leverage ForwardedHeaderFilter.
Spring Boot users may use the server.use-forward-headers
property to configure the application.
See the Spring Boot documentation for further details.
6. Project Modules and Dependencies
In Spring Security 3.0, the codebase was sub-divided into separate jars which more clearly separate different functionality areas and third-party dependencies.
If you use Maven to build your project, these are the modules you should add to your pom.xml
.
Even if you do not use Maven, we recommend that you consult the pom.xml
files to get an idea of third-party dependencies and versions.
Another good idea is to examine the libraries that are included in the sample applications.
This section provides a reference of the modules in Spring Security and the additional dependencies that they require in order to function in a running application. We don’t include dependencies that are only used when building or testing Spring Security itself. Nor do we include transitive dependencies which are required by external dependencies.
The version of Spring required is listed on the project website, so the specific versions are omitted for Spring dependencies below. Note that some of the dependencies listed as "optional" below may still be required for other non-security functionality in a Spring application. Also dependencies listed as "optional" may not actually be marked as such in the project’s Maven POM files if they are used in most applications. They are "optional" only in the sense that you don’t need them unless you are using the specified functionality.
Where a module depends on another Spring Security module, the non-optional dependencies of the module it depends on are also assumed to be required and are not listed separately.
6.1. Core — spring-security-core.jar
This module contains core authentication and access-contol classes and interfaces, remoting support, and basic provisioning APIs. It is required by any application that uses Spring Security. It supports standalone applications, remote clients, method (service layer) security, and JDBC user provisioning. It contains the following top-level packages:
-
org.springframework.security.core
-
org.springframework.security.access
-
org.springframework.security.authentication
-
org.springframework.security.provisioning
Dependency | Version | Description |
---|---|---|
ehcache |
1.6.2 |
Required if the Ehcache-based user cache implementation is used (optional). |
spring-aop |
Method security is based on Spring AOP |
|
spring-beans |
Required for Spring configuration |
|
spring-expression |
Required for expression-based method security (optional) |
|
spring-jdbc |
Required if using a database to store user data (optional). |
|
spring-tx |
Required if using a database to store user data (optional). |
|
aspectjrt |
1.6.10 |
Required if using AspectJ support (optional). |
jsr250-api |
1.0 |
Required if you are using JSR-250 method-security annotations (optional). |
6.2. Remoting — spring-security-remoting.jar
This module provides integration with Spring Remoting.
You do not need this unless you are writing a remote client that uses Spring Remoting.
The main package is org.springframework.security.remoting
.
Dependency | Version | Description |
---|---|---|
spring-security-core |
||
spring-web |
Required for clients which use HTTP remoting support. |
6.3. Web — spring-security-web.jar
This module contains filters and related web-security infrastructure code.
It contains anything with a servlet API dependency.
You need it if you require Spring Security web authentication services and URL-based access-control.
The main package is org.springframework.security.web
.
Dependency | Version | Description |
---|---|---|
spring-security-core |
||
spring-web |
Spring web support classes are used extensively. |
|
spring-jdbc |
Required for JDBC-based persistent remember-me token repository (optional). |
|
spring-tx |
Required by remember-me persistent token repository implementations (optional). |
6.4. Config — spring-security-config.jar
This module contains the security namespace parsing code and Java configuration code.
You need it if you use the Spring Security XML namespace for configuration or Spring Security’s Java Configuration support.
The main package is org.springframework.security.config
.
None of the classes are intended for direct use in an application.
Dependency | Version | Description |
---|---|---|
spring-security-core |
||
spring-security-web |
Required if you are using any web-related namespace configuration (optional). |
|
spring-security-ldap |
Required if you are using the LDAP namespace options (optional). |
|
spring-security-openid |
Required if you are using OpenID authentication (optional). |
|
aspectjweaver |
1.6.10 |
Required if using the protect-pointcut namespace syntax (optional). |
6.5. LDAP — spring-security-ldap.jar
This module provides LDAP authentication and provisioning code.
It is required if you need to use LDAP authentication or manage LDAP user entries.
The top-level package is org.springframework.security.ldap
.
Dependency | Version | Description |
---|---|---|
spring-security-core |
||
spring-ldap-core |
1.3.0 |
LDAP support is based on Spring LDAP. |
spring-tx |
Data exception classes are required. |
|
apache-ds [1] |
1.5.5 |
Required if you are using an embedded LDAP server (optional). |
shared-ldap |
0.9.15 |
Required if you are using an embedded LDAP server (optional). |
ldapsdk |
4.1 |
Mozilla LdapSDK. Used for decoding LDAP password policy controls if you are using password-policy functionality with OpenLDAP, for example. |
6.6. OAuth 2.0 Core — spring-security-oauth2-core.jar
spring-security-oauth2-core.jar
contains core classes and interfaces that provide support for the OAuth 2.0 Authorization Framework and for OpenID Connect Core 1.0.
It is required by applications that use OAuth 2.0 or OpenID Connect Core 1.0, such as client, resource server, and authorization server.
The top-level package is org.springframework.security.oauth2.core
.
6.7. OAuth 2.0 Client — spring-security-oauth2-client.jar
spring-security-oauth2-client.jar
contains Spring Security’s client support for OAuth 2.0 Authorization Framework and OpenID Connect Core 1.0.
It is required by applications that use OAuth 2.0 Login or OAuth Client support.
The top-level package is org.springframework.security.oauth2.client
.
6.8. OAuth 2.0 JOSE — spring-security-oauth2-jose.jar
spring-security-oauth2-jose.jar
contains Spring Security’s support for the JOSE (Javascript Object Signing and Encryption) framework.
The JOSE framework is intended to provide a method to securely transfer claims between parties.
It is built from a collection of specifications:
-
JSON Web Token (JWT)
-
JSON Web Signature (JWS)
-
JSON Web Encryption (JWE)
-
JSON Web Key (JWK)
It contains the following top-level packages:
-
org.springframework.security.oauth2.jwt
-
org.springframework.security.oauth2.jose
6.9. OAuth 2.0 Resource Server — spring-security-oauth2-resource-server.jar
spring-security-oauth2-resource-server.jar
contains Spring Security’s support for OAuth 2.0 Resource Servers.
It is used to protect APIs via OAuth 2.0 Bearer Tokens.
The top-level package is org.springframework.security.oauth2.server.resource
.
6.10. ACL — spring-security-acl.jar
This module contains a specialized domain object ACL implementation.
It is used to apply security to specific domain object instances within your application.
The top-level package is org.springframework.security.acls
.
Dependency | Version | Description |
---|---|---|
spring-security-core |
||
ehcache |
1.6.2 |
Required if the Ehcache-based ACL cache implementation is used (optional if you are using your own implementation). |
spring-jdbc |
Required if you are using the default JDBC-based AclService (optional if you implement your own). |
|
spring-tx |
Required if you are using the default JDBC-based AclService (optional if you implement your own). |
6.11. CAS — spring-security-cas.jar
This module contains Spring Security’s CAS client integration.
You should use it if you want to use Spring Security web authentication with a CAS single sign-on server.
The top-level package is org.springframework.security.cas
.
Dependency | Version | Description |
---|---|---|
spring-security-core |
||
spring-security-web |
||
cas-client-core |
3.1.12 |
The JA-SIG CAS Client. This is the basis of the Spring Security integration. |
ehcache |
1.6.2 |
Required if you are using the Ehcache-based ticket cache (optional). |
6.12. OpenID — spring-security-openid.jar
The OpenID 1.0 and 2.0 protocols have been deprecated and users are encouraged to migrate to OpenID Connect, which is supported by spring-security-oauth2. |
This module contains OpenID web authentication support.
It is used to authenticate users against an external OpenID server.
The top-level package is org.springframework.security.openid
.
It requires OpenID4Java.
Dependency | Version | Description |
---|---|---|
spring-security-core |
||
spring-security-web |
||
openid4java-nodeps |
0.9.6 |
Spring Security’s OpenID integration uses OpenID4Java. |
httpclient |
4.1.1 |
openid4java-nodeps depends on HttpClient 4. |
guice |
2.0 |
openid4java-nodeps depends on Guice 2. |
6.13. Test — spring-security-test.jar
This module contains support for testing with Spring Security.
6.14. Taglibs — spring-secuity-taglibs.jar
Provides Spring Security’s JSP tag implementations.
Dependency | Version | Description |
---|---|---|
spring-security-core |
||
spring-security-web |
||
spring-security-acl |
Required if you are using the |
|
spring-expression |
Required if you are using SPEL expressions in your tag access constraints. |
7. Samples
Spring Security includes many samples applications.
These samples are being migrated to a separate project, however, you can still find the not migrated samples in an older branch of the Spring Security repository. |
Servlet Applications
Filter
. This means it works with any application that runs in a Servlet Container. More concretely, you do not need to use Spring in your Servlet-based application to take advantage of Spring Security.
8. Hello Spring Security
This section covers the minimum setup for how to use Spring Security with Spring Boot.
The completed application can be found in our samples repository. For your convenience, you can download a minimal Spring Boot + Spring Security application by clicking here. |
8.2. Starting Hello Spring Security Boot
You can now run the Spring Boot application by using the Maven Plugin’s run
goal.
The following example shows how to do so (and the beginning of the output from doing so):
$ ./mvn spring-boot:run
...
INFO 23689 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
...
8.3. Spring Boot Auto Configuration
Spring Boot automatically:
-
Enables Spring Security’s default configuration, which creates a servlet
Filter
as a bean namedspringSecurityFilterChain
. This bean is responsible for all the security (protecting the application URLs, validating submitted username and passwords, redirecting to the log in form, and so on) within your application. -
Creates a
UserDetailsService
bean with a username ofuser
and a randomly generated password that is logged to the console. -
Registers the
Filter
with a bean namedspringSecurityFilterChain
with the Servlet container for every request.
Spring Boot is not configuring much, but it does a lot. A summary of the features follows:
-
Require an authenticated user for any interaction with the application
-
Generate a default login form for you
-
Let the user with a username of
user
and a password that is logged to the console to authenticate with form-based authentication (in the preceding example, the password is8e557245-73e2-4286-969a-ff57fe326336
) -
Protects the password storage with BCrypt
-
Lets the user log out
-
CSRF attack prevention
-
Session Fixation protection
-
Security Header integration
-
HTTP Strict Transport Security for secure requests
-
X-Content-Type-Options integration
-
Cache Control (can be overridden later by your application to allow caching of your static resources)
-
X-XSS-Protection integration
-
X-Frame-Options integration to help prevent Clickjacking
-
-
Integrate with the following Servlet API methods:
9. Servlet Security: The Big Picture
This section discusses Spring Security’s high level architecture within Servlet based applications. We build on this high level understanding within Authentication, Authorization, Protection Against Exploits sections of the reference.
9.1. A Review of Filter
s
Spring Security’s Servlet support is based on Servlet Filter
s, so it is helpful to look at the role of Filter
s generally first.
The picture below shows the typical layering of the handlers for a single HTTP request.
The client sends a request to the application, and the container creates a FilterChain
which contains the Filter
s and Servlet
that should process the HttpServletRequest
based on the path of the request URI.
In a Spring MVC application the Servlet
is an instance of DispatcherServlet
.
At most one Servlet
can handle a single HttpServletRequest
and HttpServletResponse
.
However, more than one Filter
can be used to:
-
Prevent downstream
Filter
s or theServlet
from being invoked. In this instance theFilter
will typically write theHttpServletResponse
. -
Modify the
HttpServletRequest
orHttpServletResponse
used by the downstreamFilter
s andServlet
The power of the Filter
comes from the FilterChain
that is passed into it.
FilterChain
Usage Examplepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
// do something before the rest of the application
chain.doFilter(request, response) // invoke the rest of the application
// do something after the rest of the application
}
Since a Filter
only impacts downstream Filter
s and the Servlet
, the order each Filter
is invoked is extremely important.
9.2. DelegatingFilterProxy
Spring provides a Filter
implementation named DelegatingFilterProxy
that allows bridging between the Servlet container’s lifecycle and Spring’s ApplicationContext
.
The Servlet container allows registering Filter
s using its own standards, but it is not aware of Spring defined Beans.
DelegatingFilterProxy
can be registered via standard Servlet container mechanisms, but delegate all the work to a Spring Bean that implements Filter
.
Here is a picture of how DelegatingFilterProxy
fits into the Filter
s and the FilterChain
.
DelegatingFilterProxy
looks up Bean Filter0 from the ApplicationContext
and then invokes Bean Filter0.
The pseudo code of DelegatingFilterProxy
can be seen below.
DelegatingFilterProxy
Pseudo Codepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate
is an instance of Bean Filter0
Filter delegate = getFilterBean(someBeanName);
// delegate work to the Spring Bean
delegate.doFilter(request, response);
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate
is an instance of Bean Filter0
val delegate: Filter = getFilterBean(someBeanName)
// delegate work to the Spring Bean
delegate.doFilter(request, response)
}
Another benefit of DelegatingFilterProxy
is that it allows delaying looking Filter
bean instances.
This is important because the container needs to register the Filter
instances before the container can startup.
However, Spring typically uses a ContextLoaderListener
to load the Spring Beans which will not be done until after the Filter
instances need to be registered.
9.3. FilterChainProxy
Spring Security’s Servlet support is contained within FilterChainProxy
.
FilterChainProxy
is a special Filter
provided by Spring Security that allows delegating to many Filter
instances through SecurityFilterChain
.
Since FilterChainProxy
is a Bean, it is typically wrapped in a DelegatingFilterProxy.
9.4. SecurityFilterChain
SecurityFilterChain
is used by FilterChainProxy to determine which Spring Security Filter
s should be invoked for this request.
The Security Filters in SecurityFilterChain
are typically Beans, but they are registered with FilterChainProxy
instead of DelegatingFilterProxy.
FilterChainProxy
provides a number of advantages to registering directly with the Servlet container or DelegatingFilterProxy.
First, it provides a starting point for all of Spring Security’s Servlet support.
For that reason, if you are attempting to troubleshoot Spring Security’s Servlet support, adding a debug point in FilterChainProxy
is a great place to start.
Second, since FilterChainProxy
is central to Spring Security usage it can perform tasks that are not viewed as optional.
For example, it clears out the SecurityContext
to avoid memory leaks.
It also applies Spring Security’s HttpFirewall
to protect applications against certain types of attacks.
In addition, it provides more flexibility in determining when a SecurityFilterChain
should be invoked.
In a Servlet container, Filter
s are invoked based upon the URL alone.
However, FilterChainProxy
can determine invocation based upon anything in the HttpServletRequest
by leveraging the RequestMatcher
interface.
In fact, FilterChainProxy
can be used to determine which SecurityFilterChain
should be used.
This allows providing a totally separate configuration for different slices of your application.
In the Multiple SecurityFilterChain Figure FilterChainProxy
decides which SecurityFilterChain
should be used.
Only the first SecurityFilterChain
that matches will be invoked.
If a URL of /api/messages/
is requested, it will first match on SecurityFilterChain0
's pattern of /api/**
, so only SecurityFilterChain0
will be invoked even though it also matches on SecurityFilterChainn
.
If a URL of /messages/
is requested, it will not match on SecurityFilterChain0
's pattern of /api/**
, so FilterChainProxy
will continue trying each SecurityFilterChain
.
Assuming that no other, SecurityFilterChain
instances match SecurityFilterChainn
will be invoked.
Notice that SecurityFilterChain0
has only three security Filter
s instances configured.
However, SecurityFilterChainn
has four security Filter
s configured.
It is important to note that each SecurityFilterChain
can be unique and configured in isolation.
In fact, a SecurityFilterChain
might have zero security Filter
s if the application wants Spring Security to ignore certain requests.
9.5. Security Filters
The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API.
The order of Filter
s matters.
It is typically not necessary to know the ordering of Spring Security’s Filter
s.
However, there are times that it is beneficial to know the ordering
Below is a comprehensive list of Spring Security Filter ordering:
-
ChannelProcessingFilter
-
WebAsyncManagerIntegrationFilter
-
SecurityContextPersistenceFilter
-
HeaderWriterFilter
-
CorsFilter
-
CsrfFilter
-
LogoutFilter
-
OAuth2AuthorizationRequestRedirectFilter
-
Saml2WebSsoAuthenticationRequestFilter
-
X509AuthenticationFilter
-
AbstractPreAuthenticatedProcessingFilter
-
CasAuthenticationFilter
-
OAuth2LoginAuthenticationFilter
-
Saml2WebSsoAuthenticationFilter
-
OpenIDAuthenticationFilter
-
DefaultLoginPageGeneratingFilter
-
DefaultLogoutPageGeneratingFilter
-
ConcurrentSessionFilter
-
BearerTokenAuthenticationFilter
-
RequestCacheAwareFilter
-
SecurityContextHolderAwareRequestFilter
-
JaasApiIntegrationFilter
-
RememberMeAuthenticationFilter
-
AnonymousAuthenticationFilter
-
OAuth2AuthorizationCodeGrantFilter
-
SessionManagementFilter
-
SwitchUserFilter
9.6. Handling Security Exceptions
The ExceptionTranslationFilter
allows translation of AccessDeniedException
and AuthenticationException
into HTTP responses.
ExceptionTranslationFilter
is inserted into the FilterChainProxy as one of the Security Filters.
-
First, the
ExceptionTranslationFilter
invokesFilterChain.doFilter(request, response)
to invoke the rest of the application. -
If the user is not authenticated or it is an
AuthenticationException
, then Start Authentication.-
The SecurityContextHolder is cleared out
-
The
HttpServletRequest
is saved in theRequestCache
. When the user successfully authenticates, theRequestCache
is used to replay the original request. -
The
AuthenticationEntryPoint
is used to request credentials from the client. For example, it might redirect to a log in page or send aWWW-Authenticate
header.
-
-
Otherwise if it is an
AccessDeniedException
, then Access Denied. TheAccessDeniedHandler
is invoked to handle access denied.
If the application does not throw an |
The pseudocode for ExceptionTranslationFilter
looks something like this:
try {
filterChain.doFilter(request, response); (1)
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); (2)
} else {
accessDenied(); (3)
}
}
1 | You will recall from A Review of Filter s that invoking FilterChain.doFilter(request, response) is the equivalent of invoking the rest of the application.
This means that if another part of the application, (i.e. FilterSecurityInterceptor or method security) throws an AuthenticationException or AccessDeniedException it will be caught and handled here. |
2 | If the user is not authenticated or it is an AuthenticationException , then Start Authentication. |
3 | Otherwise, Access Denied |
10. Authentication
Spring Security provides comprehensive support for Authentication. This section discusses:
Architecture Components
This section describes the main architectural components of Spring Security’s used in Servlet authentication. If you need concrete flows that explain how these pieces fit together, look at the Authentication Mechanism specific sections.
-
SecurityContextHolder - The
SecurityContextHolder
is where Spring Security stores the details of who is authenticated. -
SecurityContext - is obtained from the
SecurityContextHolder
and contains theAuthentication
of the currently authenticated user. -
Authentication - Can be the input to
AuthenticationManager
to provide the credentials a user has provided to authenticate or the current user from theSecurityContext
. -
GrantedAuthority - An authority that is granted to the principal on the
Authentication
(i.e. roles, scopes, etc.) -
AuthenticationManager - the API that defines how Spring Security’s Filters perform authentication.
-
ProviderManager - the most common implementation of
AuthenticationManager
. -
AuthenticationProvider - used by
ProviderManager
to perform a specific type of authentication. -
Request Credentials with
AuthenticationEntryPoint
- used for requesting credentials from a client (i.e. redirecting to a log in page, sending aWWW-Authenticate
response, etc.) -
AbstractAuthenticationProcessingFilter - a base
Filter
used for authentication. This also gives a good idea of the high level flow of authentication and how pieces work together.
Authentication Mechanisms
-
Username and Password - how to authenticate with a username/password
-
OAuth 2.0 Login - OAuth 2.0 Log In with OpenID Connect and non-standard OAuth 2.0 Login (i.e. GitHub)
-
SAML 2.0 Login - SAML 2.0 Log In
-
Central Authentication Server (CAS) - Central Authentication Server (CAS) Support
-
Remember Me - How to remember a user past session expiration
-
JAAS Authentication - Authenticate with JAAS
-
OpenID - OpenID Authentication (not to be confused with OpenID Connect)
-
Pre-Authentication Scenarios - Authenticate with an external mechanism such as SiteMinder or Java EE security but still use Spring Security for authorization and protection against common exploits.
-
X509 Authentication - X509 Authentication
10.1. SecurityContextHolder
At the heart of Spring Security’s authentication model is the SecurityContextHolder
.
It contains the SecurityContext.
The SecurityContextHolder
is where Spring Security stores the details of who is authenticated.
Spring Security does not care how the SecurityContextHolder
is populated.
If it contains a value, then it is used as the currently authenticated user.
The simplest way to indicate a user is authenticated is to set the SecurityContextHolder
directly.
SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext(); (1)
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER"); (2)
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context); (3)
val context: SecurityContext = SecurityContextHolder.createEmptyContext() (1)
val authentication: Authentication = TestingAuthenticationToken("username", "password", "ROLE_USER") (2)
context.authentication = authentication
SecurityContextHolder.setContext(context) (3)
1 | We start by creating an empty SecurityContext .
It is important to create a new SecurityContext instance instead of using SecurityContextHolder.getContext().setAuthentication(authentication) to avoid race conditions across multiple threads. |
2 | Next we create a new Authentication object.
Spring Security does not care what type of Authentication implementation is set on the SecurityContext .
Here we use TestingAuthenticationToken because it is very simple.
A more common production scenario is UsernamePasswordAuthenticationToken(userDetails, password, authorities) . |
3 | Finally, we set the SecurityContext on the SecurityContextHolder .
Spring Security will use this information for authorization. |
If you wish to obtain information about the authenticated principal, you can do so by accessing the SecurityContextHolder
.
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
val context = SecurityContextHolder.getContext()
val authentication = context.authentication
val username = authentication.name
val principal = authentication.principal
val authorities = authentication.authorities
By default the SecurityContextHolder
uses a ThreadLocal
to store these details, which means that the SecurityContext
is always available to methods in the same thread, even if the SecurityContext
is not explicitly passed around as an argument to those methods.
Using a ThreadLocal
in this way is quite safe if care is taken to clear the thread after the present principal’s request is processed.
Spring Security’s FilterChainProxy ensures that the SecurityContext
is always cleared.
Some applications aren’t entirely suitable for using a ThreadLocal
, because of the specific way they work with threads.
For example, a Swing client might want all threads in a Java Virtual Machine to use the same security context.
SecurityContextHolder
can be configured with a strategy on startup to specify how you would like the context to be stored.
For a standalone application you would use the SecurityContextHolder.MODE_GLOBAL
strategy.
Other applications might want to have threads spawned by the secure thread also assume the same security identity.
This is achieved by using SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
.
You can change the mode from the default SecurityContextHolder.MODE_THREADLOCAL
in two ways.
The first is to set a system property, the second is to call a static method on SecurityContextHolder
.
Most applications won’t need to change from the default, but if you do, take a look at the JavaDoc for SecurityContextHolder
to learn more.
10.2. SecurityContext
The SecurityContext
is obtained from the SecurityContextHolder.
The SecurityContext
contains an Authentication object.
10.3. Authentication
The Authentication
serves two main purposes within Spring Security:
-
An input to
AuthenticationManager
to provide the credentials a user has provided to authenticate. When used in this scenario,isAuthenticated()
returnsfalse
. -
Represents the currently authenticated user. The current
Authentication
can be obtained from the SecurityContext.
The Authentication
contains:
-
principal
- identifies the user. When authenticating with a username/password this is often an instance ofUserDetails
. -
credentials
- Often a password. In many cases this will be cleared after the user is authenticated to ensure it is not leaked. -
authorities
- theGrantedAuthority
s are high level permissions the user is granted. A few examples are roles or scopes.
10.4. GrantedAuthority
GrantedAuthority
s are high level permissions the user is granted. A few examples are roles or scopes.
GrantedAuthority
s can be obtained from the Authentication.getAuthorities()
method.
This method provides a Collection
of GrantedAuthority
objects.
A GrantedAuthority
is, not surprisingly, an authority that is granted to the principal.
Such authorities are usually "roles", such as ROLE_ADMINISTRATOR
or ROLE_HR_SUPERVISOR
.
These roles are later on configured for web authorization, method authorization and domain object authorization.
Other parts of Spring Security are capable of interpreting these authorities, and expect them to be present.
When using username/password based authentication GrantedAuthority
s are usually loaded by the UserDetailsService
.
Usually the GrantedAuthority
objects are application-wide permissions.
They are not specific to a given domain object.
Thus, you wouldn’t likely have a GrantedAuthority
to represent a permission to Employee
object number 54, because if there are thousands of such authorities you would quickly run out of memory (or, at the very least, cause the application to take a long time to authenticate a user).
Of course, Spring Security is expressly designed to handle this common requirement, but you’d instead use the project’s domain object security capabilities for this purpose.
10.5. AuthenticationManager
AuthenticationManager
is the API that defines how Spring Security’s Filters perform authentication.
The Authentication
that is returned is then set on the SecurityContextHolder by the controller (i.e. Spring Security’s Filters
s) that invoked the AuthenticationManager
.
If you are not integrating with Spring Security’s Filters
s you can set the SecurityContextHolder
directly and are not required to use an AuthenticationManager
.
While the implementation of AuthenticationManager
could be anything, the most common implementation is ProviderManager
.
10.6. ProviderManager
ProviderManager
is the most commonly used implementation of AuthenticationManager
.
ProviderManager
delegates to a List
of AuthenticationProvider
s.
Each AuthenticationProvider
has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream AuthenticationProvider
to decide.
If none of the configured AuthenticationProvider
s can authenticate, then authentication will fail with a ProviderNotFoundException
which is a special AuthenticationException
that indicates the ProviderManager
was not configured to support the type of Authentication
that was passed into it.
In practice each AuthenticationProvider
knows how to perform a specific type of authentication.
For example, one AuthenticationProvider
might be able to validate a username/password, while another might be able to authenticate a SAML assertion.
This allows each AuthenticationProvider
to do a very specific type of authentication, while supporting multiple types of authentication and only exposing a single AuthenticationManager
bean.
ProviderManager
also allows configuring an optional parent AuthenticationManager
which is consulted in the event that no AuthenticationProvider
can perform authentication.
The parent can be any type of AuthenticationManager
, but it is often an instance of ProviderManager
.
In fact, multiple ProviderManager
instances might share the same parent AuthenticationManager
.
This is somewhat common in scenarios where there are multiple SecurityFilterChain
instances that have some authentication in common (the shared parent AuthenticationManager
), but also different authentication mechanisms (the different ProviderManager
instances).
By default ProviderManager
will attempt to clear any sensitive credentials information from the Authentication
object which is returned by a successful authentication request.
This prevents information like passwords being retained longer than necessary in the HttpSession
.
This may cause issues when you are using a cache of user objects, for example, to improve performance in a stateless application.
If the Authentication
contains a reference to an object in the cache (such as a UserDetails
instance) and this has its credentials removed, then it will no longer be possible to authenticate against the cached value.
You need to take this into account if you are using a cache.
An obvious solution is to make a copy of the object first, either in the cache implementation or in the AuthenticationProvider
which creates the returned Authentication
object.
Alternatively, you can disable the eraseCredentialsAfterAuthentication
property on ProviderManager
.
See the Javadoc for more information.
10.7. AuthenticationProvider
Multiple AuthenticationProvider
s can be injected into ProviderManager
.
Each AuthenticationProvider
performs a specific type of authentication.
For example, DaoAuthenticationProvider
supports username/password based authentication while JwtAuthenticationProvider
supports authenticating a JWT token.
10.8. Request Credentials with AuthenticationEntryPoint
AuthenticationEntryPoint
is used to send an HTTP response that requests credentials from a client.
Sometimes a client will proactively include credentials such as a username/password to request a resource. In these cases, Spring Security does not need to provide an HTTP response that requests credentials from the client since they are already included.
In other cases, a client will make an unauthenticated request to a resource that they are not authorized to access.
In this case, an implementation of AuthenticationEntryPoint
is used to request credentials from the client.
The AuthenticationEntryPoint
implementation might perform a redirect to a log in page, respond with an WWW-Authenticate header, etc.
10.9. AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
is used as a base Filter
for authenticating a user’s credentials.
Before the credentials can be authenticated, Spring Security typically requests the credentials using AuthenticationEntryPoint
.
Next, the AbstractAuthenticationProcessingFilter
can authenticate any authentication requests that are submitted to it.
When the user submits their credentials, the AbstractAuthenticationProcessingFilter
creates an Authentication
from the HttpServletRequest
to be authenticated.
The type of Authentication
created depends on the subclass of AbstractAuthenticationProcessingFilter
.
For example, UsernamePasswordAuthenticationFilter
creates a UsernamePasswordAuthenticationToken
from a username and password that are submitted in the HttpServletRequest
.
Next, the Authentication
is passed into the AuthenticationManager
to be authenticated.
If authentication fails, then Failure
-
The SecurityContextHolder is cleared out.
-
RememberMeServices.loginFail
is invoked. If remember me is not configured, this is a no-op. -
AuthenticationFailureHandler
is invoked.
If authentication is successful, then Success.
-
SessionAuthenticationStrategy
is notified of a new log in. -
The Authentication is set on the SecurityContextHolder. Later the
SecurityContextPersistenceFilter
saves theSecurityContext
to theHttpSession
. -
RememberMeServices.loginSuccess
is invoked. If remember me is not configured, this is a no-op. -
ApplicationEventPublisher
publishes anInteractiveAuthenticationSuccessEvent
. -
AuthenticationSuccessHandler
is invoked.
10.10. Username/Password Authentication
One of the most common ways to authenticate a user is by validating a username and password. As such, Spring Security provides comprehensive support for authenticating with a username and password.
Reading the Username & Password
Spring Security provides the following built in mechanisms for reading a username and password from the HttpServletRequest
:
Storage Mechanisms
Each of the supported mechanisms for reading a username and password can leverage any of the supported storage mechanisms:
-
Simple Storage with In-Memory Authentication
-
Relational Databases with JDBC Authentication
-
Custom data stores with UserDetailsService
-
LDAP storage with LDAP Authentication
10.10.1. Form Login
Spring Security provides support for username and password being provided through an html form. This section provides details on how form based authentication works within Spring Security.
Let’s take a look at how form based log in works within Spring Security. First, we see how the user is redirected to the log in form.
The figure builds off our SecurityFilterChain
diagram.
First, a user makes an unauthenticated request to the resource /private
for which it is not authorized.
Spring Security’s FilterSecurityInterceptor
indicates that the unauthenticated request is Denied by throwing an AccessDeniedException
.
Since the user is not authenticated, ExceptionTranslationFilter
initiates Start Authentication and sends a redirect to the log in page with the configured AuthenticationEntryPoint
.
In most cases the AuthenticationEntryPoint
is an instance of LoginUrlAuthenticationEntryPoint
.
The browser will then request the log in page that it was redirected to.
Something within the application, must render the log in page.
When the username and password are submitted, the UsernamePasswordAuthenticationFilter
authenticates the username and password.
The UsernamePasswordAuthenticationFilter
extends AbstractAuthenticationProcessingFilter, so this diagram should look pretty similar.
The figure builds off our SecurityFilterChain
diagram.
When the user submits their username and password, the UsernamePasswordAuthenticationFilter
creates a UsernamePasswordAuthenticationToken
which is a type of Authentication
by extracting the username and password from the HttpServletRequest
.
Next, the UsernamePasswordAuthenticationToken
is passed into the AuthenticationManager
to be authenticated.
The details of what AuthenticationManager
look like depend on how the user information is stored.
If authentication fails, then Failure
-
The SecurityContextHolder is cleared out.
-
RememberMeServices.loginFail
is invoked. If remember me is not configured, this is a no-op. -
AuthenticationFailureHandler
is invoked.
If authentication is successful, then Success.
-
SessionAuthenticationStrategy
is notified of a new log in. -
The Authentication is set on the SecurityContextHolder.
-
RememberMeServices.loginSuccess
is invoked. If remember me is not configured, this is a no-op. -
ApplicationEventPublisher
publishes anInteractiveAuthenticationSuccessEvent
. -
The
AuthenticationSuccessHandler
is invoked. Typically this is aSimpleUrlAuthenticationSuccessHandler
which will redirect to a request saved byExceptionTranslationFilter
when we redirect to the log in page.
Spring Security form log in is enabled by default. However, as soon as any servlet based configuration is provided, form based log in must be explicitly provided. A minimal, explicit Java configuration can be found below:
protected void configure(HttpSecurity http) {
http
// ...
.formLogin(withDefaults());
}
<http>
<!-- ... -->
<form-login />
</http>
fun configure(http: HttpSecurity) {
http {
// ...
formLogin { }
}
}
In this configuration Spring Security will render a default log in page. Most production applications will require a custom log in form.
The configuration below demonstrates how to provide a custom log in form.
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
}
<http>
<!-- ... -->
<intercept-url pattern="/login" access="permitAll" />
<form-login login-page="/login" />
</http>
fun configure(http: HttpSecurity) {
http {
// ...
formLogin {
loginPage = "/login"
permitAll()
}
}
}
When the login page is specified in the Spring Security configuration, you are responsible for rendering the page.
Below is a Thymeleaf template that produces an HTML login form that complies with a login page of /login
.:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Please Log In</title>
</head>
<body>
<h1>Please Log In</h1>
<div th:if="${param.error}">
Invalid username and password.</div>
<div th:if="${param.logout}">
You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="Username"/>
</div>
<div>
<input type="password" name="password" placeholder="Password"/>
</div>
<input type="submit" value="Log in" />
</form>
</body>
</html>
There are a few key points about the default HTML form:
-
The form should perform a
post
to/login
-
The form will need to include a CSRF Token which is automatically included by Thymeleaf.
-
The form should specify the username in a parameter named
username
-
The form should specify the password in a parameter named
password
-
If the HTTP parameter error is found, it indicates the user failed to provide a valid username / password
-
If the HTTP parameter logout is found, it indicates the user has logged out successfully
Many users will not need much more than to customize the log in page. However, if needed everything above can be customized with additional configuration.
If you are using Spring MVC, you will need a controller that maps GET /login
to the login template we created.
A minimal sample LoginController
can be see below:
@Controller
class LoginController {
@GetMapping("/login")
String login() {
return "login";
}
}
@Controller
class LoginController {
@GetMapping("/login")
fun login(): String {
return "login"
}
}
10.10.2. Basic Authentication
This section provides details on how Spring Security provides support for Basic HTTP Authentication for servlet based applications.
Let’s take a look at how HTTP Basic Authentication works within Spring Security. First, we see the WWW-Authenticate header is sent back to an unauthenticated client.
The figure builds off our SecurityFilterChain
diagram.
First, a user makes an unauthenticated request to the resource /private
for which it is not authorized.
Spring Security’s FilterSecurityInterceptor
indicates that the unauthenticated request is Denied by throwing an AccessDeniedException
.
Since the user is not authenticated, ExceptionTranslationFilter
initiates Start Authentication.
The configured AuthenticationEntryPoint
is an instance of BasicAuthenticationEntryPoint
which sends a WWW-Authenticate header.
The RequestCache
is typically a NullRequestCache
that does not save the request since the client is capable of replaying the requests it originally requested.
When a client receives the WWW-Authenticate header it knows it should retry with a username and password. Below is the flow for the username and password being processed.
The figure builds off our SecurityFilterChain
diagram.
When the user submits their username and password, the BasicAuthenticationFilter
creates a UsernamePasswordAuthenticationToken
which is a type of Authentication
by extracting the username and password from the HttpServletRequest
.
Next, the UsernamePasswordAuthenticationToken
is passed into the AuthenticationManager
to be authenticated.
The details of what AuthenticationManager
look like depend on how the user information is stored.
If authentication fails, then Failure
-
The SecurityContextHolder is cleared out.
-
RememberMeServices.loginFail
is invoked. If remember me is not configured, this is a no-op. -
AuthenticationEntryPoint
is invoked to trigger the WWW-Authenticate to be sent again.
If authentication is successful, then Success.
-
The Authentication is set on the SecurityContextHolder.
-
RememberMeServices.loginSuccess
is invoked. If remember me is not configured, this is a no-op. -
The
BasicAuthenticationFilter
invokesFilterChain.doFilter(request,response)
to continue with the rest of the application logic.
Spring Security’s HTTP Basic Authentication support in is enabled by default. However, as soon as any servlet based configuration is provided, HTTP Basic must be explicitly provided.
A minimal, explicit configuration can be found below:
protected void configure(HttpSecurity http) {
http
// ...
.httpBasic(withDefaults());
}
<http>
<!-- ... -->
<http-basic />
</http>
fun configure(http: HttpSecurity) {
http {
// ...
httpBasic { }
}
}
10.10.3. Digest Authentication
This section provides details on how Spring Security provides support for Digest Authentication which is provided DigestAuthenticationFilter
.
You should not use Digest Authentication in modern applications because it is not considered secure. The most obvious problem is that you must store your passwords in plaintext, encrypted, or an MD5 format. All of these storage formats are considered insecure. Instead, you should store credentials using a one way adaptive password hash (i.e. bCrypt, PBKDF2, SCrypt, etc) which is not supported by Digest Authentication. |
Digest Authentication attempts to solve many of the weaknesses of Basic authentication, specifically by ensuring credentials are never sent in clear text across the wire. Many browsers support Digest Authentication.
The standard governing HTTP Digest Authentication is defined by RFC 2617, which updates an earlier version of the Digest Authentication standard prescribed by RFC 2069.
Most user agents implement RFC 2617.
Spring Security’s Digest Authentication support is compatible with the “auth” quality of protection (qop
) prescribed by RFC 2617, which also provides backward compatibility with RFC 2069.
Digest Authentication was seen as a more attractive option if you need to use unencrypted HTTP (i.e. no TLS/HTTPS) and wish to maximise security of the authentication process.
However, everyone should use HTTPS.
Central to Digest Authentication is a "nonce". This is a value the server generates. Spring Security’s nonce adopts the following format:
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime: The date and time when the nonce expires, expressed in milliseconds
key: A private key to prevent modification of the nonce token
You will need to ensure you configure insecure plain text Password Storage using NoOpPasswordEncoder`. The following provides an example of configuring Digest Authentication with Java Configuration:
@Autowired
UserDetailsService userDetailsService;
DigestAuthenticationEntryPoint entryPoint() {
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
result.setRealmName("My App Relam");
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
}
DigestAuthenticationFilter digestAuthenticationFilter() {
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
result.setUserDetailsService(userDetailsService);
result.setAuthenticationEntryPoint(entryPoint());
}
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
.addFilterBefore(digestFilter());
}
<b:bean id="digestFilter"
class="org.springframework.security.web.authentication.www.DigestAuthenticationFilter"
p:userDetailsService-ref="jdbcDaoImpl"
p:authenticationEntryPoint-ref="digestEntryPoint"
/>
<b:bean id="digestEntryPoint"
class="org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint"
p:realmName="My App Realm"
p:key="3028472b-da34-4501-bfd8-a355c42bdf92"
/>
<http>
<!-- ... -->
<custom-filter ref="userFilter" position="DIGEST_AUTH_FILTER"/>
</http>
10.10.4. In-Memory Authentication
Spring Security’s InMemoryUserDetailsManager
implements UserDetailsService to provide support for username/password based authentication that is retrieved in memory.
InMemoryUserDetailsManager
provides management of UserDetails
by implementing the UserDetailsManager
interface.
UserDetails
based authentication is used by Spring Security when it is configured to accept a username/password for authentication.
In this sample we use Spring Boot CLI to encode the password of password
and get the encoded password of {bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW
.
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
<user-service>
<user name="user"
password="{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW"
authorities="ROLE_USER" />
<user name="admin"
password="{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW"
authorities="ROLE_USER,ROLE_ADMIN" />
</user-service>
@Bean
fun users(): UserDetailsService {
val user = User.builder()
.username("user")
.password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build()
val admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build()
return InMemoryUserDetailsManager(user, admin)
}
The samples above store the passwords in a secure format, but leave a lot to be desired in terms of getting started experience.
In the sample below we leverage User.withDefaultPasswordEncoder to ensure that the password stored in memory is protected.
However, it does not protect against obtaining the password by decompiling the source code.
For this reason, User.withDefaultPasswordEncoder
should only be used for "getting started" and is not intended for production.
@Bean
public UserDetailsService users() {
// The builder will ensure the passwords are encoded before saving in memory
UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
fun users(): UserDetailsService {
// The builder will ensure the passwords are encoded before saving in memory
val users = User.withDefaultPasswordEncoder()
val user = users
.username("user")
.password("password")
.roles("USER")
.build()
val admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build()
return InMemoryUserDetailsManager(user, admin)
}
There is no simple way to use User.withDefaultPasswordEncoder
with XML based configuration.
For demos or just getting started, you can choose to prefix the password with {noop}
to indicate no encoding should be used.
{noop}
XML Configuration<user-service>
<user name="user"
password="{noop}password"
authorities="ROLE_USER" />
<user name="admin"
password="{noop}password"
authorities="ROLE_USER,ROLE_ADMIN" />
</user-service>
10.10.5. JDBC Authentication
Spring Security’s JdbcDaoImpl
implements UserDetailsService to provide support for username/password based authentication that is retrieved using JDBC.
JdbcUserDetailsManager
extends JdbcDaoImpl
to provide management of UserDetails
through the UserDetailsManager
interface.
UserDetails
based authentication is used by Spring Security when it is configured to accept a username/password for authentication.
In the following sections we will discuss:
-
The Default Schema used by Spring Security JDBC Authentication
Default Schema
Spring Security provides default queries for JDBC based authentication. This section provides the corresponding default schemas used with the default queries. You will need to adjust the schema to match any customizations to the queries and the database dialect you are using.
User Schema
JdbcDaoImpl
requires tables to load the password, account status (enabled or disabled) and a list of authorities (roles) for the user.
The default schema required can be found below.
The default schema is also exposed as a classpath resource named |
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(500) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
Oracle is a popular database choice, but requires a slightly different schema. You can find the default Oracle Schema for users below.
CREATE TABLE USERS (
USERNAME NVARCHAR2(128) PRIMARY KEY,
PASSWORD NVARCHAR2(128) NOT NULL,
ENABLED CHAR(1) CHECK (ENABLED IN ('Y','N') ) NOT NULL
);
CREATE TABLE AUTHORITIES (
USERNAME NVARCHAR2(128) NOT NULL,
AUTHORITY NVARCHAR2(128) NOT NULL
);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_UNIQUE UNIQUE (USERNAME, AUTHORITY);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_FK1 FOREIGN KEY (USERNAME) REFERENCES USERS (USERNAME) ENABLE;
Group Schema
If your application is leveraging groups, you will need to provide the groups schema. The default schema for groups can be found below.
create table groups (
id bigint generated by default as identity(start with 0) primary key,
group_name varchar_ignorecase(50) not null
);
create table group_authorities (
group_id bigint not null,
authority varchar(50) not null,
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);
create table group_members (
id bigint generated by default as identity(start with 0) primary key,
username varchar(50) not null,
group_id bigint not null,
constraint fk_group_members_group foreign key(group_id) references groups(id)
);
Setting up a DataSource
Before we configure JdbcUserDetailsManager
, we must create a DataSource
.
In our example, we will setup an embedded DataSource that is initialized with the default user schema.
@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(H2)
.addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
.build();
}
<jdbc:embedded-database>
<jdbc:script location="classpath:org/springframework/security/core/userdetails/jdbc/users.ddl"/>
</jdbc:embedded-database>
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(H2)
.addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
.build()
}
In a production environment, you will want to ensure you setup a connection to an external database.
JdbcUserDetailsManager Bean
In this sample we use Spring Boot CLI to encode the password of password
and get the encoded password of {bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW
.
See the PasswordEncoder section for more details about how to store passwords.
@Bean
UserDetailsManager users(DataSource dataSource) {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
users.createUser(admin);
return users;
}
<jdbc-user-service>
<user name="user"
password="{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW"
authorities="ROLE_USER" />
<user name="admin"
password="{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW"
authorities="ROLE_USER,ROLE_ADMIN" />
</jdbc-user-service>
@Bean
fun users(dataSource: DataSource): UserDetailsManager {
val user = User.builder()
.username("user")
.password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
val admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
val users = JdbcUserDetailsManager(dataSource)
users.createUser(user)
users.createUser(admin)
return users
}
10.10.6. UserDetails
UserDetails
is returned by the UserDetailsService
.
The DaoAuthenticationProvider
validates the UserDetails
and then returns an Authentication
that has a principal that is the UserDetails
returned by the configured UserDetailsService
.
10.10.7. UserDetailsService
UserDetailsService
is used by DaoAuthenticationProvider
for retrieving a username, password, and other attributes for authenticating with a username and password.
Spring Security provides in-memory and JDBC implementations of UserDetailsService
.
You can define custom authentication by exposing a custom UserDetailsService
as a bean.
For example, the following will customize authentication assuming that CustomUserDetailsService
implements UserDetailsService
:
This is only used if the AuthenticationManagerBuilder has not been populated and no AuthenticationProviderBean is defined.
|
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
<b:bean class="example.CustomUserDetailsService"/>
@Bean
fun customUserDetailsService() = CustomUserDetailsService()
10.10.8. PasswordEncoder
Spring Security’s servlet support storing passwords securely by integrating with PasswordEncoder
.
Customizing the PasswordEncoder
implementation used by Spring Security can be done by exposing a PasswordEncoder
Bean.
10.10.9. DaoAuthenticationProvider
DaoAuthenticationProvider
is an AuthenticationProvider
implementation that leverages a UserDetailsService
and PasswordEncoder
to authenticate a username and password.
Let’s take a look at how DaoAuthenticationProvider
works within Spring Security.
The figure explains details of how the AuthenticationManager
in figures from Reading the Username & Password works.
DaoAuthenticationProvider
Usage The authentication Filter
from Reading the Username & Password passes a UsernamePasswordAuthenticationToken
to the AuthenticationManager
which is implemented by ProviderManager
.
The ProviderManager
is configured to use an AuthenticationProvider of type DaoAuthenticationProvider
.
DaoAuthenticationProvider
looks up the UserDetails
from the UserDetailsService
.
DaoAuthenticationProvider
then uses the PasswordEncoder
to validate the password on the UserDetails
returned in the previous step.
When authentication is successful, the Authentication
that is returned is of type UsernamePasswordAuthenticationToken
and has a principal that is the UserDetails
returned by the configured UserDetailsService
.
Ultimately, the returned UsernamePasswordAuthenticationToken
will be set on the SecurityContextHolder
by the authentication Filter
.
10.10.10. LDAP Authentication
LDAP is often used by organizations as a central repository for user information and as an authentication service. It can also be used to store the role information for application users.
Spring Security’s LDAP based authentication is used by Spring Security when it is configured to accept a username/password for authentication.
However, despite leveraging a username/password for authentication it does not integrate using UserDetailsService
because in bind authentication the LDAP server does not return the password so the application cannot perform validation of the password.
There are many different scenarios for how an LDAP server may be configured so Spring Security’s LDAP provider is fully configurable. It uses separate strategy interfaces for authentication and role retrieval and provides default implementations which can be configured to handle a wide range of situations.
Prerequisites
You should be familiar with LDAP before trying to use it with Spring Security. The following link provides a good introduction to the concepts involved and a guide to setting up a directory using the free LDAP server OpenLDAP: https://www.zytrax.com/books/ldap/. Some familiarity with the JNDI APIs used to access LDAP from Java may also be useful. We don’t use any third-party LDAP libraries (Mozilla, JLDAP etc.) in the LDAP provider, but extensive use is made of Spring LDAP, so some familiarity with that project may be useful if you plan on adding your own customizations.
When using LDAP authentication, it is important to ensure that you configure LDAP connection pooling properly. If you are unfamiliar with how to do this, you can refer to the Java LDAP documentation.
Setting up an Embedded LDAP Server
The first thing you will need to do is to ensure that you have an LDAP Server to point your configuration to. For simplicity, it often best to start with an embedded LDAP Server. Spring Security supports using either:
In the samples below, we expose the following as users.ldif
as a classpath resource to initialize the embedded LDAP server with the users user
and admin
both of which have a password of password
.
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups
dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people
dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password
dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password
dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
uniqueMember: uid=user,ou=people,dc=springframework,dc=org
dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
Embedded UnboundID Server
If you wish to use UnboundID, then specify the following dependencies:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.14</version>
<scope>runtime</scope>
</dependency>
depenendencies {
runtimeOnly "com.unboundid:unboundid-ldapsdk:4.0.14"
}
You can then configure the Embedded LDAP Server
@Bean
UnboundIdContainer ldapContainer() {
return new UnboundIdContainer("dc=springframework,dc=org",
"classpath:users.ldif");
}
<b:bean class="org.springframework.security.ldap.server.UnboundIdContainer"
c:defaultPartitionSuffix="dc=springframework,dc=org"
c:ldif="classpath:users.ldif"/>
@Bean
fun ldapContainer(): UnboundIdContainer {
return UnboundIdContainer("dc=springframework,dc=org","classpath:users.ldif")
}
Embedded ApacheDS Server
Spring Security uses ApacheDS 1.x which is no longer maintained. Unfortunately, ApacheDS 2.x has only released milestone versions with no stable release. Once a stable release of ApacheDS 2.x is available, we will consider updating. |
If you wish to use Apache DS, then specify the following dependencies:
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core</artifactId>
<version>1.5.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-jndi</artifactId>
<version>1.5.5</version>
<scope>runtime</scope>
</dependency>
depenendencies {
runtimeOnly "org.apache.directory.server:apacheds-core:1.5.5"
runtimeOnly "org.apache.directory.server:apacheds-server-jndi:1.5.5"
}
You can then configure the Embedded LDAP Server
@Bean
ApacheDSContainer ldapContainer() {
return new ApacheDSContainer("dc=springframework,dc=org",
"classpath:users.ldif");
}
<b:bean class="org.springframework.security.ldap.server.ApacheDSContainer"
c:defaultPartitionSuffix="dc=springframework,dc=org"
c:ldif="classpath:users.ldif"/>
@Bean
fun ldapContainer(): ApacheDSContainer {
return ApacheDSContainer("dc=springframework,dc=org", "classpath:users.ldif")
}
LDAP ContextSource
Once you have an LDAP Server to point your configuration to, you need configure Spring Security to point to an LDAP server that should be used to authenticate users.
This is done by creating an LDAP ContextSource
, which is the equivalent of a JDBC DataSource
.
ContextSource contextSource(UnboundIdContainer container) {
return new DefaultSpringSecurityContextSource("ldap://localhost:53389/dc=springframework,dc=org");
}
<ldap-server
url="ldap://localhost:53389/dc=springframework,dc=org" />
fun contextSource(container: UnboundIdContainer): ContextSource {
return DefaultSpringSecurityContextSource("ldap://localhost:53389/dc=springframework,dc=org")
}
Authentication
Spring Security’s LDAP support does not use the UserDetailsService because LDAP bind authentication does not allow clients to read the password or even a hashed version of the password. This means there is no way a password to be read and then authenticated by Spring Security.
For this reason, LDAP support is implemented using the LdapAuthenticator
interface.
The LdapAuthenticator
is also responsible for retrieving any required user attributes.
This is because the permissions on the attributes may depend on the type of authentication being used.
For example, if binding as the user, it may be necessary to read them with the user’s own permissions.
There are two LdapAuthenticator
implementations supplied with Spring Security:
Using Bind Authentication
Bind Authentication is the most common mechanism for authenticating users with LDAP. In bind authentication the users credentials (i.e. username/password) are submitted to the LDAP server which authenticates them. The advantage to using bind authentication is that the user’s secrets (i.e. password) do not need to be exposed to clients which helps to protect them from leaking.
An example of bind authentication configuration can be found below.
@Bean
BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
BindAuthenticator authenticator = new BindAuthenticator(contextSource);
authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" });
return authenticator;
}
@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
return new LdapAuthenticationProvider(authenticator);
}
<ldap-authentication-provider
user-dn-pattern="uid={0},ou=people"/>
@Bean
fun authenticator(contextSource: BaseLdapPathContextSource): BindAuthenticator {
val authenticator = BindAuthenticator(contextSource)
authenticator.setUserDnPatterns(arrayOf("uid={0},ou=people"))
return authenticator
}
@Bean
fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider {
return LdapAuthenticationProvider(authenticator)
}
This simple example would obtain the DN for the user by substituting the user login name in the supplied pattern and attempting to bind as that user with the login password. This is OK if all your users are stored under a single node in the directory. If instead you wished to configure an LDAP search filter to locate the user, you could use the following:
@Bean
BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
String searchBase = "ou=people";
String filter = "(uid={0})";
FilterBasedLdapUserSearch search =
new FilterBasedLdapUserSearch(searchBase, filter, contextSource);
BindAuthenticator authenticator = new BindAuthenticator(contextSource);
authenticator.setUserSearch(search);
return authenticator;
}
@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
return new LdapAuthenticationProvider(authenticator);
}
<ldap-authentication-provider
user-search-filter="(uid={0})"
user-search-base="ou=people"/>
@Bean
fun authenticator(contextSource: BaseLdapPathContextSource): BindAuthenticator {
val searchBase = "ou=people"
val filter = "(uid={0})"
val search = FilterBasedLdapUserSearch(searchBase, filter, contextSource)
val authenticator = BindAuthenticator(contextSource)
authenticator.setUserSearch(search)
return authenticator
}
@Bean
fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider {
return LdapAuthenticationProvider(authenticator)
}
If used with the ContextSource
definition above, this would perform a search under the DN ou=people,dc=springframework,dc=org
using (uid={0})
as a filter.
Again the user login name is substituted for the parameter in the filter name, so it will search for an entry with the uid
attribute equal to the user name.
If a user search base isn’t supplied, the search will be performed from the root.
Using Password Authentication
Password comparison is when the password supplied by the user is compared with the one stored in the repository. This can either be done by retrieving the value of the password attribute and checking it locally or by performing an LDAP "compare" operation, where the supplied password is passed to the server for comparison and the real password value is never retrieved. An LDAP compare cannot be done when the password is properly hashed with a random salt.
@Bean
PasswordComparisonAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
return new PasswordComparisonAuthenticator(contextSource);
}
@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
return new LdapAuthenticationProvider(authenticator);
}
<ldap-authentication-provider
user-dn-pattern="uid={0},ou=people">
<password-compare />
</ldap-authentication-provider>
@Bean
fun authenticator(contextSource: BaseLdapPathContextSource): PasswordComparisonAuthenticator {
return PasswordComparisonAuthenticator(contextSource)
}
@Bean
fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider {
return LdapAuthenticationProvider(authenticator)
}
A more advanced configuration with some customizations can be found below.
@Bean
PasswordComparisonAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
PasswordComparisonAuthenticator authenticator =
new PasswordComparisonAuthenticator(contextSource);
authenticator.setPasswordAttributeName("pwd"); (1)
authenticator.setPasswordEncoder(new BCryptPasswordEncoder()); (2)
return authenticator;
}
@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
return new LdapAuthenticationProvider(authenticator);
}
<ldap-authentication-provider
user-dn-pattern="uid={0},ou=people">
<password-compare password-attribute="pwd"> (1)
<password-encoder ref="passwordEncoder" /> (2)
</password-compare>
</ldap-authentication-provider>
<b:bean id="passwordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
@Bean
fun authenticator(contextSource: BaseLdapPathContextSource): PasswordComparisonAuthenticator {
val authenticator = PasswordComparisonAuthenticator(contextSource)
authenticator.setPasswordAttributeName("pwd") (1)
authenticator.setPasswordEncoder(BCryptPasswordEncoder()) (2)
return authenticator
}
@Bean
fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider {
return LdapAuthenticationProvider(authenticator)
}
1 | Specify the password attribute as pwd |
2 | Use BCryptPasswordEncoder |
LdapAuthoritiesPopulator
Spring Security’s LdapAuthoritiesPopulator
is used to determine what authorites are returned for the user.
@Bean
LdapAuthoritiesPopulator authorities(BaseLdapPathContextSource contextSource) {
String groupSearchBase = "";
DefaultLdapAuthoritiesPopulator authorities =
new DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase);
authorities.setGroupSearchFilter("member={0}");
return authorities;
}
@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator, LdapAuthoritiesPopulator authorities) {
return new LdapAuthenticationProvider(authenticator, authorities);
}
<ldap-authentication-provider
user-dn-pattern="uid={0},ou=people"
group-search-filter="member={0}"/>
@Bean
fun authorities(contextSource: BaseLdapPathContextSource): LdapAuthoritiesPopulator {
val groupSearchBase = ""
val authorities = DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase)
authorities.setGroupSearchFilter("member={0}")
return authorities
}
@Bean
fun authenticationProvider(authenticator: LdapAuthenticator, authorities: LdapAuthoritiesPopulator): LdapAuthenticationProvider {
return LdapAuthenticationProvider(authenticator, authorities)
}
Active Directory
Active Directory supports its own non-standard authentication options, and the normal usage pattern doesn’t fit too cleanly with the standard LdapAuthenticationProvider
.
Typically authentication is performed using the domain username (in the form user@domain
), rather than using an LDAP distinguished name.
To make this easier, Spring Security has an authentication provider which is customized for a typical Active Directory setup.
Configuring ActiveDirectoryLdapAuthenticationProvider
is quite straightforward.
You just need to supply the domain name and an LDAP URL supplying the address of the server [2].
An example configuration can be seen below:
@Bean
ActiveDirectoryLdapAuthenticationProvider authenticationProvider() {
return new ActiveDirectoryLdapAuthenticationProvider("example.com", "ldap://company.example.com/");
}
<bean id="authenticationProvider"
class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
<constructor-arg value="example.com" />
<constructor-arg value="ldap://company.example.com/" />
</bean>
@Bean
fun authenticationProvider(): ActiveDirectoryLdapAuthenticationProvider {
return ActiveDirectoryLdapAuthenticationProvider("example.com", "ldap://company.example.com/")
}
10.11. Session Management
HTTP session related functionality is handled by a combination of the SessionManagementFilter
and the SessionAuthenticationStrategy
interface, which the filter delegates to.
Typical usage includes session-fixation protection attack prevention, detection of session timeouts and restrictions on how many sessions an authenticated user may have open concurrently.
10.11.1. Detecting Timeouts
You can configure Spring Security to detect the submission of an invalid session ID and redirect the user to an appropriate URL.
This is achieved through the session-management
element:
<http>
...
<session-management invalid-session-url="/invalidSession.htm" />
</http>
Note that if you use this mechanism to detect session timeouts, it may falsely report an error if the user logs out and then logs back in without closing the browser. This is because the session cookie is not cleared when you invalidate the session and will be resubmitted even if the user has logged out. You may be able to explicitly delete the JSESSIONID cookie on logging out, for example by using the following syntax in the logout handler:
<http>
<logout delete-cookies="JSESSIONID" />
</http>
Unfortunately this can’t be guaranteed to work with every servlet container, so you will need to test it in your environment
If you are running your application behind a proxy, you may also be able to remove the session cookie by configuring the proxy server.
For example, using Apache HTTPD’s mod_headers, the following directive would delete the
|
10.11.2. Concurrent Session Control
If you wish to place constraints on a single user’s ability to log in to your application, Spring Security supports this out of the box with the following simple additions.
First you need to add the following listener to your web.xml
file to keep Spring Security updated about session lifecycle events:
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
Then add the following lines to your application context:
<http>
...
<session-management>
<concurrency-control max-sessions="1" />
</session-management>
</http>
This will prevent a user from logging in multiple times - a second login will cause the first to be invalidated. Often you would prefer to prevent a second login, in which case you can use
<http>
...
<session-management>
<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>
The second login will then be rejected.
By "rejected", we mean that the user will be sent to the authentication-failure-url
if form-based login is being used.
If the second authentication takes place through another non-interactive mechanism, such as "remember-me", an "unauthorized" (401) error will be sent to the client.
If instead you want to use an error page, you can add the attribute session-authentication-error-url
to the session-management
element.
If you are using a customized authentication filter for form-based login, then you have to configure concurrent session control support explicitly. More details can be found in the Session Management chapter.
10.11.3. Session Fixation Attack Protection
Session fixation attacks are a potential risk where it is possible for a malicious attacker to create a session by accessing a site, then persuade another user to log in with the same session (by sending them a link containing the session identifier as a parameter, for example).
Spring Security protects against this automatically by creating a new session or otherwise changing the session ID when a user logs in.
If you don’t require this protection, or it conflicts with some other requirement, you can control the behavior using the session-fixation-protection
attribute on <session-management>
, which has four options
-
none
- Don’t do anything. The original session will be retained. -
newSession
- Create a new "clean" session, without copying the existing session data (Spring Security-related attributes will still be copied). -
migrateSession
- Create a new session and copy all existing session attributes to the new session. This is the default in Servlet 3.0 or older containers. -
changeSessionId
- Do not create a new session. Instead, use the session fixation protection provided by the Servlet container (HttpServletRequest#changeSessionId()
). This option is only available in Servlet 3.1 (Java EE 7) and newer containers. Specifying it in older containers will result in an exception. This is the default in Servlet 3.1 and newer containers.
When session fixation protection occurs, it results in a SessionFixationProtectionEvent
being published in the application context.
If you use changeSessionId
, this protection will also result in any javax.servlet.http.HttpSessionIdListener
s being notified, so use caution if your code listens for both events.
See the Session Management chapter for additional information.
10.11.4. SessionManagementFilter
The SessionManagementFilter
checks the contents of the SecurityContextRepository
against the current contents of the SecurityContextHolder
to determine whether a user has been authenticated during the current request, typically by a non-interactive authentication mechanism, such as pre-authentication or remember-me [3].
If the repository contains a security context, the filter does nothing.
If it doesn’t, and the thread-local SecurityContext
contains a (non-anonymous) Authentication
object, the filter assumes they have been authenticated by a previous filter in the stack.
It will then invoke the configured SessionAuthenticationStrategy
.
If the user is not currently authenticated, the filter will check whether an invalid session ID has been requested (because of a timeout, for example) and will invoke the configured InvalidSessionStrategy
, if one is set.
The most common behaviour is just to redirect to a fixed URL and this is encapsulated in the standard implementation SimpleRedirectInvalidSessionStrategy
.
The latter is also used when configuring an invalid session URL through the namespace,as described earlier.
10.11.5. SessionAuthenticationStrategy
SessionAuthenticationStrategy
is used by both SessionManagementFilter
and AbstractAuthenticationProcessingFilter
, so if you are using a customized form-login class, for example, you will need to inject it into both of these.
In this case, a typical configuration, combining the namespace and custom beans might look like this:
<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>
<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
...
</beans:bean>
<beans:bean id="sas" class=
"org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
Note that the use of the default, SessionFixationProtectionStrategy
may cause issues if you are storing beans in the session which implement HttpSessionBindingListener
, including Spring session-scoped beans.
See the Javadoc for this class for more information.
10.11.6. Concurrency Control
Spring Security is able to prevent a principal from concurrently authenticating to the same application more than a specified number of times. Many ISVs take advantage of this to enforce licensing, whilst network administrators like this feature because it helps prevent people from sharing login names. You can, for example, stop user "Batman" from logging onto the web application from two different sessions. You can either expire their previous login or you can report an error when they try to log in again, preventing the second login. Note that if you are using the second approach, a user who has not explicitly logged out (but who has just closed their browser, for example) will not be able to log in again until their original session expires.
Concurrency control is supported by the namespace, so please check the earlier namespace chapter for the simplest configuration. Sometimes you need to customize things though.
The implementation uses a specialized version of SessionAuthenticationStrategy
, called ConcurrentSessionControlAuthenticationStrategy
.
Previously the concurrent authentication check was made by the |
To use concurrent session support, you’ll need to add the following to web.xml
:
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
In addition, you will need to add the ConcurrentSessionFilter
to your FilterChainProxy
.
The ConcurrentSessionFilter
requires two constructor arguments, sessionRegistry
, which generally points to an instance of SessionRegistryImpl
, and sessionInformationExpiredStrategy
, which defines the strategy to apply when a session has expired.
A configuration using the namespace to create the FilterChainProxy
and other default beans might look like this:
<http>
<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>
<beans:bean id="redirectSessionInformationExpiredStrategy"
class="org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy">
<beans:constructor-arg name="invalidSessionUrl" value="/session-expired.htm" />
</beans:bean>
<beans:bean id="concurrencyFilter"
class="org.springframework.security.web.session.ConcurrentSessionFilter">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<beans:constructor-arg name="sessionInformationExpiredStrategy" ref="redirectSessionInformationExpiredStrategy" />
</beans:bean>
<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>
<beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<beans:constructor-arg>
<beans:list>
<beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<beans:constructor-arg ref="sessionRegistry"/>
<beans:property name="maximumSessions" value="1" />
<beans:property name="exceptionIfMaximumExceeded" value="true" />
</beans:bean>
<beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
</beans:bean>
<beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
<beans:constructor-arg ref="sessionRegistry"/>
</beans:bean>
</beans:list>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="sessionRegistry"
class="org.springframework.security.core.session.SessionRegistryImpl" />
Adding the listener to web.xml
causes an ApplicationEvent
to be published to the Spring ApplicationContext
every time a HttpSession
commences or ends.
This is critical, as it allows the SessionRegistryImpl
to be notified when a session ends.
Without it, a user will never be able to log back in again once they have exceeded their session allowance, even if they log out of another session or it times out.
Querying the SessionRegistry for currently authenticated users and their sessions
Setting up concurrency-control, either through the namespace or using plain beans has the useful side effect of providing you with a reference to the SessionRegistry
which you can use directly within your application, so even if you don’t want to restrict the number of sessions a user may have, it may be worth setting up the infrastructure anyway.
You can set the maximumSession
property to -1 to allow unlimited sessions.
If you’re using the namespace, you can set an alias for the internally-created SessionRegistry
using the session-registry-alias
attribute, providing a reference which you can inject into your own beans.
The getAllPrincipals()
method supplies you with a list of the currently authenticated users.
You can list a user’s sessions by calling the getAllSessions(Object principal, boolean includeExpiredSessions)
method, which returns a list of SessionInformation
objects.
You can also expire a user’s session by calling expireNow()
on a SessionInformation
instance.
When the user returns to the application, they will be prevented from proceeding.
You may find these methods useful in an administration application, for example.
Have a look at the Javadoc for more information.
10.12. Remember-Me Authentication
10.12.1. Overview
Remember-me or persistent-login authentication refers to web sites being able to remember the identity of a principal between sessions. This is typically accomplished by sending a cookie to the browser, with the cookie being detected during future sessions and causing automated login to take place. Spring Security provides the necessary hooks for these operations to take place, and has two concrete remember-me implementations. One uses hashing to preserve the security of cookie-based tokens and the other uses a database or other persistent storage mechanism to store the generated tokens.
Note that both implementations require a UserDetailsService
.
If you are using an authentication provider which doesn’t use a UserDetailsService
(for example, the LDAP provider) then it won’t work unless you also have a UserDetailsService
bean in your application context.
10.12.2. Simple Hash-Based Token Approach
This approach uses hashing to achieve a useful remember-me strategy. In essence a cookie is sent to the browser upon successful interactive authentication, with the cookie being composed as follows:
base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))
username: As identifiable to the UserDetailsService
password: That matches the one in the retrieved UserDetails
expirationTime: The date and time when the remember-me token expires, expressed in milliseconds
key: A private key to prevent modification of the remember-me token
As such the remember-me token is valid only for the period specified, and provided that the username, password and key does not change. Notably, this has a potential security issue in that a captured remember-me token will be usable from any user agent until such time as the token expires. This is the same issue as with digest authentication. If a principal is aware a token has been captured, they can easily change their password and immediately invalidate all remember-me tokens on issue. If more significant security is needed you should use the approach described in the next section. Alternatively remember-me services should simply not be used at all.
If you are familiar with the topics discussed in the chapter on namespace configuration, you can enable remember-me authentication just by adding the <remember-me>
element:
<http>
...
<remember-me key="myAppKey"/>
</http>
The UserDetailsService
will normally be selected automatically.
If you have more than one in your application context, you need to specify which one should be used with the user-service-ref
attribute, where the value is the name of your UserDetailsService
bean.
10.12.3. Persistent Token Approach
This approach is based on the article http://jaspan.com/improved_persistent_login_cookie_best_practice with some minor modifications [4]. To use the this approach with namespace configuration, you would supply a datasource reference:
<http>
...
<remember-me data-source-ref="someDataSource"/>
</http>
The database should contain a persistent_logins
table, created using the following SQL (or equivalent):
create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null)
10.12.4. Remember-Me Interfaces and Implementations
Remember-me is used with UsernamePasswordAuthenticationFilter
, and is implemented via hooks in the AbstractAuthenticationProcessingFilter
superclass.
It is also used within BasicAuthenticationFilter
.
The hooks will invoke a concrete RememberMeServices
at the appropriate times.
The interface looks like this:
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
void loginFail(HttpServletRequest request, HttpServletResponse response);
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
Please refer to the Javadoc for a fuller discussion on what the methods do, although note at this stage that AbstractAuthenticationProcessingFilter
only calls the loginFail()
and loginSuccess()
methods.
The autoLogin()
method is called by RememberMeAuthenticationFilter
whenever the SecurityContextHolder
does not contain an Authentication
.
This interface therefore provides the underlying remember-me implementation with sufficient notification of authentication-related events, and delegates to the implementation whenever a candidate web request might contain a cookie and wish to be remembered.
This design allows any number of remember-me implementation strategies.
We’ve seen above that Spring Security provides two implementations.
We’ll look at these in turn.
TokenBasedRememberMeServices
This implementation supports the simpler approach described in Simple Hash-Based Token Approach.
TokenBasedRememberMeServices
generates a RememberMeAuthenticationToken
, which is processed by RememberMeAuthenticationProvider
.
A key
is shared between this authentication provider and the TokenBasedRememberMeServices
.
In addition, TokenBasedRememberMeServices
requires A UserDetailsService from which it can retrieve the username and password for signature comparison purposes, and generate the RememberMeAuthenticationToken
to contain the correct GrantedAuthority
s.
Some sort of logout command should be provided by the application that invalidates the cookie if the user requests this.
TokenBasedRememberMeServices
also implements Spring Security’s LogoutHandler
interface so can be used with LogoutFilter
to have the cookie cleared automatically.
The beans required in an application context to enable remember-me services are as follows:
<bean id="rememberMeFilter" class=
"org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices"/>
<property name="authenticationManager" ref="theAuthenticationManager" />
</bean>
<bean id="rememberMeServices" class=
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="myUserDetailsService"/>
<property name="key" value="springRocks"/>
</bean>
<bean id="rememberMeAuthenticationProvider" class=
"org.springframework.security.authentication.RememberMeAuthenticationProvider">
<property name="key" value="springRocks"/>
</bean>
Don’t forget to add your RememberMeServices
implementation to your UsernamePasswordAuthenticationFilter.setRememberMeServices()
property, include the RememberMeAuthenticationProvider
in your AuthenticationManager.setProviders()
list, and add RememberMeAuthenticationFilter
into your FilterChainProxy
(typically immediately after your UsernamePasswordAuthenticationFilter
).
PersistentTokenBasedRememberMeServices
This class can be used in the same way as TokenBasedRememberMeServices
, but it additionally needs to be configured with a PersistentTokenRepository
to store the tokens.
There are two standard implementations.
-
InMemoryTokenRepositoryImpl
which is intended for testing only. -
JdbcTokenRepositoryImpl
which stores the tokens in a database.
The database schema is described above in Persistent Token Approach.
10.13. OpenID Support
The OpenID 1.0 and 2.0 protocols have been deprecated and users are encouraged to migrate to OpenID Connect, which is supported by spring-security-oauth2. |
The namespace supports OpenID login either instead of, or in addition to normal form-based login, with a simple change:
<http>
<intercept-url pattern="/**" access="ROLE_USER" />
<openid-login />
</http>
You should then register yourself with an OpenID provider (such as myopenid.com), and add the user information to your in-memory <user-service>
:
<user name="https://jimi.hendrix.myopenid.com/" authorities="ROLE_USER" />
You should be able to login using the myopenid.com
site to authenticate.
It is also possible to select a specific UserDetailsService
bean for use OpenID by setting the user-service-ref
attribute on the openid-login
element.
Note that we have omitted the password attribute from the above user configuration, since this set of user data is only being used to load the authorities for the user.
A random password will be generated internally, preventing you from accidentally using this user data as an authentication source elsewhere in your configuration.
10.13.1. Attribute Exchange
Support for OpenID attribute exchange. As an example, the following configuration would attempt to retrieve the email and full name from the OpenID provider, for use by the application:
<openid-login>
<attribute-exchange>
<openid-attribute name="email" type="https://axschema.org/contact/email" required="true"/>
<openid-attribute name="name" type="https://axschema.org/namePerson"/>
</attribute-exchange>
</openid-login>
The "type" of each OpenID attribute is a URI, determined by a particular schema, in this case https://axschema.org/.
If an attribute must be retrieved for successful authentication, the required
attribute can be set.
The exact schema and attributes supported will depend on your OpenID provider.
The attribute values are returned as part of the authentication process and can be accessed afterwards using the following code:
OpenIDAuthenticationToken token =
(OpenIDAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
List<OpenIDAttribute> attributes = token.getAttributes();
We can obtain the OpenIDAuthenticationToken
from the SecurityContextHolder.
The OpenIDAttribute
contains the attribute type and the retrieved value (or values in the case of multi-valued attributes).
You can supply multiple attribute-exchange
elements, using an identifier-matcher
attribute on each.
This contains a regular expression which will be matched against the OpenID identifier supplied by the user.
See the OpenID sample application in the codebase for an example configuration, providing different attribute lists for the Google, Yahoo and MyOpenID providers.
10.14. Anonymous Authentication
10.14.1. Overview
It’s generally considered good security practice to adopt a "deny-by-default" where you explicitly specify what is allowed and disallow everything else.
Defining what is accessible to unauthenticated users is a similar situation, particularly for web applications.
Many sites require that users must be authenticated for anything other than a few URLs (for example the home and login pages).
In this case it is easiest to define access configuration attributes for these specific URLs rather than have for every secured resource.
Put differently, sometimes it is nice to say ROLE_SOMETHING
is required by default and only allow certain exceptions to this rule, such as for login, logout and home pages of an application.
You could also omit these pages from the filter chain entirely, thus bypassing the access control checks, but this may be undesirable for other reasons, particularly if the pages behave differently for authenticated users.
This is what we mean by anonymous authentication.
Note that there is no real conceptual difference between a user who is "anonymously authenticated" and an unauthenticated user.
Spring Security’s anonymous authentication just gives you a more convenient way to configure your access-control attributes.
Calls to servlet API calls such as getCallerPrincipal
, for example, will still return null even though there is actually an anonymous authentication object in the SecurityContextHolder
.
There are other situations where anonymous authentication is useful, such as when an auditing interceptor queries the SecurityContextHolder
to identify which principal was responsible for a given operation.
Classes can be authored more robustly if they know the SecurityContextHolder
always contains an Authentication
object, and never null
.
10.14.2. Configuration
Anonymous authentication support is provided automatically when using the HTTP configuration Spring Security 3.0 and can be customized (or disabled) using the <anonymous>
element.
You don’t need to configure the beans described here unless you are using traditional bean configuration.
Three classes that together provide the anonymous authentication feature.
AnonymousAuthenticationToken
is an implementation of Authentication
, and stores the GrantedAuthority
s which apply to the anonymous principal.
There is a corresponding AnonymousAuthenticationProvider
, which is chained into the ProviderManager
so that AnonymousAuthenticationToken
s are accepted.
Finally, there is an AnonymousAuthenticationFilter
, which is chained after the normal authentication mechanisms and automatically adds an AnonymousAuthenticationToken
to the SecurityContextHolder
if there is no existing Authentication
held there.
The definition of the filter and authentication provider appears as follows:
<bean id="anonymousAuthFilter"
class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
<property name="key" value="foobar"/>
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
</bean>
<bean id="anonymousAuthenticationProvider"
class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
<property name="key" value="foobar"/>
</bean>
The key
is shared between the filter and authentication provider, so that tokens created by the former are accepted by the latter [5].
The userAttribute
is expressed in the form of usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]
.
This is the same syntax as used after the equals sign for the userMap
property of InMemoryDaoImpl
.
As explained earlier, the benefit of anonymous authentication is that all URI patterns can have security applied to them. For example:
<bean id="filterSecurityInterceptor"
class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
<property name="securityMetadata">
<security:filter-security-metadata-source>
<security:intercept-url pattern='/index.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
<security:intercept-url pattern='/hello.htm' access='ROLE_ANONYMOUS,ROLE_USER'/>
<security:intercept-url pattern='/logoff.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
<security:intercept-url pattern='/login.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
<security:intercept-url pattern='/**' access='ROLE_USER'/>
</security:filter-security-metadata-source>" +
</property>
</bean>
10.14.3. AuthenticationTrustResolver
Rounding out the anonymous authentication discussion is the AuthenticationTrustResolver
interface, with its corresponding AuthenticationTrustResolverImpl
implementation.
This interface provides an isAnonymous(Authentication)
method, which allows interested classes to take into account this special type of authentication status.
The ExceptionTranslationFilter
uses this interface in processing AccessDeniedException
s.
If an AccessDeniedException
is thrown, and the authentication is of an anonymous type, instead of throwing a 403 (forbidden) response, the filter will instead commence the AuthenticationEntryPoint
so the principal can authenticate properly.
This is a necessary distinction, otherwise principals would always be deemed "authenticated" and never be given an opportunity to login via form, basic, digest or some other normal authentication mechanism.
You will often see the ROLE_ANONYMOUS
attribute in the above interceptor configuration replaced with IS_AUTHENTICATED_ANONYMOUSLY
, which is effectively the same thing when defining access controls.
This is an example of the use of the AuthenticatedVoter
which we will see in the authorization chapter.
It uses an AuthenticationTrustResolver
to process this particular configuration attribute and grant access to anonymous users.
The AuthenticatedVoter
approach is more powerful, since it allows you to differentiate between anonymous, remember-me and fully-authenticated users.
If you don’t need this functionality though, then you can stick with ROLE_ANONYMOUS
, which will be processed by Spring Security’s standard RoleVoter
.
10.14.4. Getting Anonymous Authentications with Spring MVC
Spring MVC resolves parameters of type Principal
using its own argument resolver.
This means that a construct like this one:
@GetMapping("/")
public String method(Authentication authentication) {
if (authentication instanceof AnonymousAuthenticationToken) {
return "anonymous";
} else {
return "not anonymous";
}
}
@GetMapping("/")
fun method(authentication: Authentication?): String {
return if (authentication is AnonymousAuthenticationToken) {
"anonymous"
} else {
"not anonymous"
}
}
will always return "not anonymous", even for anonymous requests.
The reason is that Spring MVC resolves the parameter using HttpServletRequest#getPrincipal
, which is null
when the request is anonymous.
If you’d like to obtain the Authentication
in anonymous requests, use @CurrentSecurityContext
instead:
@GetMapping("/")
public String method(@CurrentSecurityContext SecurityContext context) {
return context.getAuthentication().getName();
}
@GetMapping("/")
fun method(@CurrentSecurityContext context : SecurityContext) : String =
context!!.authentication!!.name
10.15. Pre-Authentication Scenarios
There are situations where you want to use Spring Security for authorization, but the user has already been reliably authenticated by some external system prior to accessing the application. We refer to these situations as "pre-authenticated" scenarios. Examples include X.509, Siteminder and authentication by the Java EE container in which the application is running. When using pre-authentication, Spring Security has to
-
Identify the user making the request.
-
Obtain the authorities for the user.
The details will depend on the external authentication mechanism.
A user might be identified by their certificate information in the case of X.509, or by an HTTP request header in the case of Siteminder.
If relying on container authentication, the user will be identified by calling the getUserPrincipal()
method on the incoming HTTP request.
In some cases, the external mechanism may supply role/authority information for the user but in others the authorities must be obtained from a separate source, such as a UserDetailsService
.
10.15.1. Pre-Authentication Framework Classes
Because most pre-authentication mechanisms follow the same pattern, Spring Security has a set of classes which provide an internal framework for implementing pre-authenticated authentication providers.
This removes duplication and allows new implementations to be added in a structured fashion, without having to write everything from scratch.
You don’t need to know about these classes if you want to use something like X.509 authentication, as it already has a namespace configuration option which is simpler to use and get started with.
If you need to use explicit bean configuration or are planning on writing your own implementation then an understanding of how the provided implementations work will be useful.
You will find classes under the org.springframework.security.web.authentication.preauth
.
We just provide an outline here so you should consult the Javadoc and source where appropriate.
AbstractPreAuthenticatedProcessingFilter
This class will check the current contents of the security context and, if empty, it will attempt to extract user information from the HTTP request and submit it to the AuthenticationManager
.
Subclasses override the following methods to obtain this information:
protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);
protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);
protected abstract fun getPreAuthenticatedPrincipal(request: HttpServletRequest): Any?
protected abstract fun getPreAuthenticatedCredentials(request: HttpServletRequest): Any?
After calling these, the filter will create a PreAuthenticatedAuthenticationToken
containing the returned data and submit it for authentication.
By "authentication" here, we really just mean further processing to perhaps load the user’s authorities, but the standard Spring Security authentication architecture is followed.
Like other Spring Security authentication filters, the pre-authentication filter has an authenticationDetailsSource
property which by default will create a WebAuthenticationDetails
object to store additional information such as the session-identifier and originating IP address in the details
property of the Authentication
object.
In cases where user role information can be obtained from the pre-authentication mechanism, the data is also stored in this property, with the details implementing the GrantedAuthoritiesContainer
interface.
This enables the authentication provider to read the authorities which were externally allocated to the user.
We’ll look at a concrete example next.
J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource
If the filter is configured with an authenticationDetailsSource
which is an instance of this class, the authority information is obtained by calling the isUserInRole(String role)
method for each of a pre-determined set of "mappable roles".
The class gets these from a configured MappableAttributesRetriever
.
Possible implementations include hard-coding a list in the application context and reading the role information from the <security-role>
information in a web.xml
file.
The pre-authentication sample application uses the latter approach.
There is an additional stage where the roles (or attributes) are mapped to Spring Security GrantedAuthority
objects using a configured Attributes2GrantedAuthoritiesMapper
.
The default will just add the usual ROLE_
prefix to the names, but it gives you full control over the behaviour.
PreAuthenticatedAuthenticationProvider
The pre-authenticated provider has little more to do than load the UserDetails
object for the user.
It does this by delegating to an AuthenticationUserDetailsService
.
The latter is similar to the standard UserDetailsService
but takes an Authentication
object rather than just user name:
public interface AuthenticationUserDetailsService {
UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;
}
This interface may have also other uses but with pre-authentication it allows access to the authorities which were packaged in the Authentication
object, as we saw in the previous section.
The PreAuthenticatedGrantedAuthoritiesUserDetailsService
class does this.
Alternatively, it may delegate to a standard UserDetailsService
via the UserDetailsByNameServiceWrapper
implementation.
Http403ForbiddenEntryPoint
The AuthenticationEntryPoint
is responsible for kick-starting the authentication process for an unauthenticated user (when they try to access a protected resource), but in the pre-authenticated case this doesn’t apply.
You would only configure the ExceptionTranslationFilter
with an instance of this class if you aren’t using pre-authentication in combination with other authentication mechanisms.
It will be called if the user is rejected by the AbstractPreAuthenticatedProcessingFilter
resulting in a null authentication.
It always returns a 403
-forbidden response code if called.
10.15.2. Concrete Implementations
X.509 authentication is covered in its own chapter. Here we’ll look at some classes which provide support for other pre-authenticated scenarios.
Request-Header Authentication (Siteminder)
An external authentication system may supply information to the application by setting specific headers on the HTTP request.
A well-known example of this is Siteminder, which passes the username in a header called SM_USER
.
This mechanism is supported by the class RequestHeaderAuthenticationFilter
which simply extracts the username from the header.
It defaults to using the name SM_USER
as the header name.
See the Javadoc for more details.
Note that when using a system like this, the framework performs no authentication checks at all and it is extremely important that the external system is configured properly and protects all access to the application. If an attacker is able to forge the headers in their original request without this being detected then they could potentially choose any username they wished. |
Siteminder Example Configuration
A typical configuration using this filter would look like this:
<security:http>
<!-- Additional http configuration omitted -->
<security:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
</security:http>
<bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
<property name="principalRequestHeader" value="SM_USER"/>
<property name="authenticationManager" ref="authenticationManager" />
</bean>
<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService">
<bean id="userDetailsServiceWrapper"
class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<property name="userDetailsService" ref="userDetailsService"/>
</bean>
</property>
</bean>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
</security:authentication-manager>
We’ve assumed here that the security namespace is being used for configuration.
It’s also assumed that you have added a UserDetailsService
(called "userDetailsService") to your configuration to load the user’s roles.
Java EE Container Authentication
The class J2eePreAuthenticatedProcessingFilter
will extract the username from the userPrincipal
property of the HttpServletRequest
.
Use of this filter would usually be combined with the use of Java EE roles as described above in J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource.
There is a sample application in the samples project which uses this approach, so get hold of the code from GitHub and have a look at the application context file if you are interested.
10.16. Java Authentication and Authorization Service (JAAS) Provider
10.16.1. Overview
Spring Security provides a package able to delegate authentication requests to the Java Authentication and Authorization Service (JAAS). This package is discussed in detail below.
10.16.2. AbstractJaasAuthenticationProvider
The AbstractJaasAuthenticationProvider
is the basis for the provided JAAS AuthenticationProvider
implementations.
Subclasses must implement a method that creates the LoginContext
.
The AbstractJaasAuthenticationProvider
has a number of dependencies that can be injected into it that are discussed below.
JAAS CallbackHandler
Most JAAS LoginModule
s require a callback of some sort.
These callbacks are usually used to obtain the username and password from the user.
In a Spring Security deployment, Spring Security is responsible for this user interaction (via the authentication mechanism).
Thus, by the time the authentication request is delegated through to JAAS, Spring Security’s authentication mechanism will already have fully-populated an Authentication
object containing all the information required by the JAAS LoginModule
.
Therefore, the JAAS package for Spring Security provides two default callback handlers, JaasNameCallbackHandler
and JaasPasswordCallbackHandler
.
Each of these callback handlers implement JaasAuthenticationCallbackHandler
.
In most cases these callback handlers can simply be used without understanding the internal mechanics.
For those needing full control over the callback behavior, internally AbstractJaasAuthenticationProvider
wraps these JaasAuthenticationCallbackHandler
s with an InternalCallbackHandler
.
The InternalCallbackHandler
is the class that actually implements JAAS normal CallbackHandler
interface.
Any time that the JAAS LoginModule
is used, it is passed a list of application context configured InternalCallbackHandler
s.
If the LoginModule
requests a callback against the InternalCallbackHandler
s, the callback is in-turn passed to the JaasAuthenticationCallbackHandler
s being wrapped.
JAAS AuthorityGranter
JAAS works with principals.
Even "roles" are represented as principals in JAAS.
Spring Security, on the other hand, works with Authentication
objects.
Each Authentication
object contains a single principal, and multiple GrantedAuthority
s.
To facilitate mapping between these different concepts, Spring Security’s JAAS package includes an AuthorityGranter
interface.
An AuthorityGranter
is responsible for inspecting a JAAS principal and returning a set of String
s, representing the authorities assigned to the principal.
For each returned authority string, the AbstractJaasAuthenticationProvider
creates a JaasGrantedAuthority
(which implements Spring Security’s GrantedAuthority
interface) containing the authority string and the JAAS principal that the AuthorityGranter
was passed.
The AbstractJaasAuthenticationProvider
obtains the JAAS principals by firstly successfully authenticating the user’s credentials using the JAAS LoginModule
, and then accessing the LoginContext
it returns.
A call to LoginContext.getSubject().getPrincipals()
is made, with each resulting principal passed to each AuthorityGranter
defined against the AbstractJaasAuthenticationProvider.setAuthorityGranters(List)
property.
Spring Security does not include any production AuthorityGranter
s given that every JAAS principal has an implementation-specific meaning.
However, there is a TestAuthorityGranter
in the unit tests that demonstrates a simple AuthorityGranter
implementation.
10.16.3. DefaultJaasAuthenticationProvider
The DefaultJaasAuthenticationProvider
allows a JAAS Configuration
object to be injected into it as a dependency.
It then creates a LoginContext
using the injected JAAS Configuration
.
This means that DefaultJaasAuthenticationProvider
is not bound any particular implementation of Configuration
as JaasAuthenticationProvider
is.
InMemoryConfiguration
In order to make it easy to inject a Configuration
into DefaultJaasAuthenticationProvider
, a default in-memory implementation named InMemoryConfiguration
is provided.
The implementation constructor accepts a Map
where each key represents a login configuration name and the value represents an Array
of AppConfigurationEntry
s.
InMemoryConfiguration
also supports a default Array
of AppConfigurationEntry
objects that will be used if no mapping is found within the provided Map
.
For details, refer to the class level javadoc of InMemoryConfiguration
.
DefaultJaasAuthenticationProvider Example Configuration
While the Spring configuration for InMemoryConfiguration
can be more verbose than the standard JAAS configuration files, using it in conjunction with DefaultJaasAuthenticationProvider
is more flexible than JaasAuthenticationProvider
since it not dependant on the default Configuration
implementation.
An example configuration of DefaultJaasAuthenticationProvider
using InMemoryConfiguration
is provided below.
Note that custom implementations of Configuration
can easily be injected into DefaultJaasAuthenticationProvider
as well.
<bean id="jaasAuthProvider"
class="org.springframework.security.authentication.jaas.DefaultJaasAuthenticationProvider">
<property name="configuration">
<bean class="org.springframework.security.authentication.jaas.memory.InMemoryConfiguration">
<constructor-arg>
<map>
<!--
SPRINGSECURITY is the default loginContextName
for AbstractJaasAuthenticationProvider
-->
<entry key="SPRINGSECURITY">
<array>
<bean class="javax.security.auth.login.AppConfigurationEntry">
<constructor-arg value="sample.SampleLoginModule" />
<constructor-arg>
<util:constant static-field=
"javax.security.auth.login.AppConfigurationEntry$LoginModuleControlFlag.REQUIRED"/>
</constructor-arg>
<constructor-arg>
<map></map>
</constructor-arg>
</bean>
</array>
</entry>
</map>
</constructor-arg>
</bean>
</property>
<property name="authorityGranters">
<list>
<!-- You will need to write your own implementation of AuthorityGranter -->
<bean class="org.springframework.security.authentication.jaas.TestAuthorityGranter"/>
</list>
</property>
</bean>
10.16.4. JaasAuthenticationProvider
The JaasAuthenticationProvider
assumes the default Configuration
is an instance of ConfigFile.
This assumption is made in order to attempt to update the Configuration
.
The JaasAuthenticationProvider
then uses the default Configuration
to create the LoginContext
.
Let’s assume we have a JAAS login configuration file, /WEB-INF/login.conf
, with the following contents:
JAASTest {
sample.SampleLoginModule required;
};
Like all Spring Security beans, the JaasAuthenticationProvider
is configured via the application context.
The following definitions would correspond to the above JAAS login configuration file:
<bean id="jaasAuthenticationProvider"
class="org.springframework.security.authentication.jaas.JaasAuthenticationProvider">
<property name="loginConfig" value="/WEB-INF/login.conf"/>
<property name="loginContextName" value="JAASTest"/>
<property name="callbackHandlers">
<list>
<bean
class="org.springframework.security.authentication.jaas.JaasNameCallbackHandler"/>
<bean
class="org.springframework.security.authentication.jaas.JaasPasswordCallbackHandler"/>
</list>
</property>
<property name="authorityGranters">
<list>
<bean class="org.springframework.security.authentication.jaas.TestAuthorityGranter"/>
</list>
</property>
</bean>
10.16.5. Running as a Subject
If configured, the JaasApiIntegrationFilter
will attempt to run as the Subject
on the JaasAuthenticationToken
.
This means that the Subject
can be accessed using:
Subject subject = Subject.getSubject(AccessController.getContext());
This integration can easily be configured using the jaas-api-provision attribute. This feature is useful when integrating with legacy or external API’s that rely on the JAAS Subject being populated.
10.17. CAS Authentication
10.17.1. Overview
JA-SIG produces an enterprise-wide single sign on system known as CAS. Unlike other initiatives, JA-SIG’s Central Authentication Service is open source, widely used, simple to understand, platform independent, and supports proxy capabilities. Spring Security fully supports CAS, and provides an easy migration path from single-application deployments of Spring Security through to multiple-application deployments secured by an enterprise-wide CAS server.
You can learn more about CAS at https://www.apereo.org. You will also need to visit this site to download the CAS Server files.
10.17.2. How CAS Works
Whilst the CAS web site contains documents that detail the architecture of CAS, we present the general overview again here within the context of Spring Security. Spring Security 3.x supports CAS 3. At the time of writing, the CAS server was at version 3.4.
Somewhere in your enterprise you will need to setup a CAS server. The CAS server is simply a standard WAR file, so there isn’t anything difficult about setting up your server. Inside the WAR file you will customise the login and other single sign on pages displayed to users.
When deploying a CAS 3.4 server, you will also need to specify an AuthenticationHandler
in the deployerConfigContext.xml
included with CAS.
The AuthenticationHandler
has a simple method that returns a boolean as to whether a given set of Credentials is valid.
Your AuthenticationHandler
implementation will need to link into some type of backend authentication repository, such as an LDAP server or database.
CAS itself includes numerous AuthenticationHandler
s out of the box to assist with this.
When you download and deploy the server war file, it is set up to successfully authenticate users who enter a password matching their username, which is useful for testing.
Apart from the CAS server itself, the other key players are of course the secure web applications deployed throughout your enterprise. These web applications are known as "services". There are three types of services. Those that authenticate service tickets, those that can obtain proxy tickets, and those that authenticate proxy tickets. Authenticating a proxy ticket differs because the list of proxies must be validated and often times a proxy ticket can be reused.
Spring Security and CAS Interaction Sequence
The basic interaction between a web browser, CAS server and a Spring Security-secured service is as follows:
-
The web user is browsing the service’s public pages. CAS or Spring Security is not involved.
-
The user eventually requests a page that is either secure or one of the beans it uses is secure. Spring Security’s
ExceptionTranslationFilter
will detect theAccessDeniedException
orAuthenticationException
. -
Because the user’s
Authentication
object (or lack thereof) caused anAuthenticationException
, theExceptionTranslationFilter
will call the configuredAuthenticationEntryPoint
. If using CAS, this will be theCasAuthenticationEntryPoint
class. -
The
CasAuthenticationEntryPoint
will redirect the user’s browser to the CAS server. It will also indicate aservice
parameter, which is the callback URL for the Spring Security service (your application). For example, the URL to which the browser is redirected might be https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas. -
After the user’s browser redirects to CAS, they will be prompted for their username and password. If the user presents a session cookie which indicates they’ve previously logged on, they will not be prompted to login again (there is an exception to this procedure, which we’ll cover later). CAS will use the
PasswordHandler
(orAuthenticationHandler
if using CAS 3.0) discussed above to decide whether the username and password is valid. -
Upon successful login, CAS will redirect the user’s browser back to the original service. It will also include a
ticket
parameter, which is an opaque string representing the "service ticket". Continuing our earlier example, the URL the browser is redirected to might be https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ. -
Back in the service web application, the
CasAuthenticationFilter
is always listening for requests to/login/cas
(this is configurable, but we’ll use the defaults in this introduction). The processing filter will construct aUsernamePasswordAuthenticationToken
representing the service ticket. The principal will be equal toCasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
, whilst the credentials will be the service ticket opaque value. This authentication request will then be handed to the configuredAuthenticationManager
. -
The
AuthenticationManager
implementation will be theProviderManager
, which is in turn configured with theCasAuthenticationProvider
. TheCasAuthenticationProvider
only responds toUsernamePasswordAuthenticationToken
s containing the CAS-specific principal (such asCasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
) andCasAuthenticationToken
s (discussed later). -
CasAuthenticationProvider
will validate the service ticket using aTicketValidator
implementation. This will typically be aCas20ServiceTicketValidator
which is one of the classes included in the CAS client library. In the event the application needs to validate proxy tickets, theCas20ProxyTicketValidator
is used. TheTicketValidator
makes an HTTPS request to the CAS server in order to validate the service ticket. It may also include a proxy callback URL, which is included in this example: https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor. -
Back on the CAS server, the validation request will be received. If the presented service ticket matches the service URL the ticket was issued to, CAS will provide an affirmative response in XML indicating the username. If any proxy was involved in the authentication (discussed below), the list of proxies is also included in the XML response.
-
[OPTIONAL] If the request to the CAS validation service included the proxy callback URL (in the
pgtUrl
parameter), CAS will include apgtIou
string in the XML response. ThispgtIou
represents a proxy-granting ticket IOU. The CAS server will then create its own HTTPS connection back to thepgtUrl
. This is to mutually authenticate the CAS server and the claimed service URL. The HTTPS connection will be used to send a proxy granting ticket to the original web application. For example, https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH. -
The
Cas20TicketValidator
will parse the XML received from the CAS server. It will return to theCasAuthenticationProvider
aTicketResponse
, which includes the username (mandatory), proxy list (if any were involved), and proxy-granting ticket IOU (if the proxy callback was requested). -
Next
CasAuthenticationProvider
will call a configuredCasProxyDecider
. TheCasProxyDecider
indicates whether the proxy list in theTicketResponse
is acceptable to the service. Several implementations are provided with Spring Security:RejectProxyTickets
,AcceptAnyCasProxy
andNamedCasProxyDecider
. These names are largely self-explanatory, exceptNamedCasProxyDecider
which allows aList
of trusted proxies to be provided. -
CasAuthenticationProvider
will next request aAuthenticationUserDetailsService
to load theGrantedAuthority
objects that apply to the user contained in theAssertion
. -
If there were no problems,
CasAuthenticationProvider
constructs aCasAuthenticationToken
including the details contained in theTicketResponse
and theGrantedAuthority
s. -
Control then returns to
CasAuthenticationFilter
, which places the createdCasAuthenticationToken
in the security context. -
The user’s browser is redirected to the original page that caused the
AuthenticationException
(or a custom destination depending on the configuration).
It’s good that you’re still here! Let’s now look at how this is configured
10.17.3. Configuration of CAS Client
The web application side of CAS is made easy due to Spring Security. It is assumed you already know the basics of using Spring Security, so these are not covered again below. We’ll assume a namespace based configuration is being used and add in the CAS beans as required. Each section builds upon the previous section. A full CAS sample application can be found in the Spring Security Samples.
Service Ticket Authentication
This section describes how to setup Spring Security to authenticate Service Tickets.
Often times this is all a web application requires.
You will need to add a ServiceProperties
bean to your application context.
This represents your CAS service:
<bean id="serviceProperties"
class="org.springframework.security.cas.ServiceProperties">
<property name="service"
value="https://localhost:8443/cas-sample/login/cas"/>
<property name="sendRenew" value="false"/>
</bean>
The service
must equal a URL that will be monitored by the CasAuthenticationFilter
.
The sendRenew
defaults to false, but should be set to true if your application is particularly sensitive.
What this parameter does is tell the CAS login service that a single sign on login is unacceptable.
Instead, the user will need to re-enter their username and password in order to gain access to the service.
The following beans should be configured to commence the CAS authentication process (assuming you’re using a namespace configuration):
<security:http entry-point-ref="casEntryPoint">
...
<security:custom-filter position="CAS_FILTER" ref="casFilter" />
</security:http>
<bean id="casFilter"
class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>
<bean id="casEntryPoint"
class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
<property name="loginUrl" value="https://localhost:9443/cas/login"/>
<property name="serviceProperties" ref="serviceProperties"/>
</bean>
For CAS to operate, the ExceptionTranslationFilter
must have its authenticationEntryPoint
property set to the CasAuthenticationEntryPoint
bean.
This can easily be done using entry-point-ref as is done in the example above.
The CasAuthenticationEntryPoint
must refer to the ServiceProperties
bean (discussed above), which provides the URL to the enterprise’s CAS login server.
This is where the user’s browser will be redirected.
The CasAuthenticationFilter
has very similar properties to the UsernamePasswordAuthenticationFilter
(used for form-based logins).
You can use these properties to customize things like behavior for authentication success and failure.
Next you need to add a CasAuthenticationProvider
and its collaborators:
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="casAuthenticationProvider" />
</security:authentication-manager>
<bean id="casAuthenticationProvider"
class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="authenticationUserDetailsService">
<bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<constructor-arg ref="userService" />
</bean>
</property>
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<constructor-arg index="0" value="https://localhost:9443/cas" />
</bean>
</property>
<property name="key" value="an_id_for_this_auth_provider_only"/>
</bean>
<security:user-service id="userService">
<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that
NoOpPasswordEncoder should be used.
This is not safe for production, but makes reading
in samples easier.
Normally passwords should be hashed using BCrypt -->
<security:user name="joe" password="{noop}joe" authorities="ROLE_USER" />
...
</security:user-service>
The CasAuthenticationProvider
uses a UserDetailsService
instance to load the authorities for a user, once they have been authenticated by CAS.
We’ve shown a simple in-memory setup here.
Note that the CasAuthenticationProvider
does not actually use the password for authentication, but it does use the authorities.
The beans are all reasonably self-explanatory if you refer back to the How CAS Works section.
This completes the most basic configuration for CAS. If you haven’t made any mistakes, your web application should happily work within the framework of CAS single sign on. No other parts of Spring Security need to be concerned about the fact CAS handled authentication. In the following sections we will discuss some (optional) more advanced configurations.
Single Logout
The CAS protocol supports Single Logout and can be easily added to your Spring Security configuration. Below are updates to the Spring Security configuration that handle Single Logout
<security:http entry-point-ref="casEntryPoint">
...
<security:logout logout-success-url="/cas-logout.jsp"/>
<security:custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<security:custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
</security:http>
<!-- This filter handles a Single Logout Request from the CAS Server -->
<bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>
<!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
<bean id="requestSingleLogoutFilter"
class="org.springframework.security.web.authentication.logout.LogoutFilter">
<constructor-arg value="https://localhost:9443/cas/logout"/>
<constructor-arg>
<bean class=
"org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
</constructor-arg>
<property name="filterProcessesUrl" value="/logout/cas"/>
</bean>
The logout
element logs the user out of the local application, but does not end the session with the CAS server or any other applications that have been logged into.
The requestSingleLogoutFilter
filter will allow the URL of /spring_security_cas_logout
to be requested to redirect the application to the configured CAS Server logout URL.
Then the CAS Server will send a Single Logout request to all the services that were signed into.
The singleLogoutFilter
handles the Single Logout request by looking up the HttpSession
in a static Map
and then invalidating it.
It might be confusing why both the logout
element and the singleLogoutFilter
are needed.
It is considered best practice to logout locally first since the SingleSignOutFilter
just stores the HttpSession
in a static Map
in order to call invalidate on it.
With the configuration above, the flow of logout would be:
-
The user requests
/logout
which would log the user out of the local application and send the user to the logout success page. -
The logout success page,
/cas-logout.jsp
, should instruct the user to click a link pointing to/logout/cas
in order to logout out of all applications. -
When the user clicks the link, the user is redirected to the CAS single logout URL (https://localhost:9443/cas/logout).
-
On the CAS Server side, the CAS single logout URL then submits single logout requests to all the CAS Services. On the CAS Service side, JASIG’s
SingleSignOutFilter
processes the logout request by invalidating the original session.
The next step is to add the following to your web.xml
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>
org.jasig.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>
When using the SingleSignOutFilter you might encounter some encoding issues.
Therefore it is recommended to add the CharacterEncodingFilter
to ensure that the character encoding is correct when using the SingleSignOutFilter
.
Again, refer to JASIG’s documentation for details.
The SingleSignOutHttpSessionListener
ensures that when an HttpSession
expires, the mapping used for single logout is removed.
Authenticating to a Stateless Service with CAS
This section describes how to authenticate to a service using CAS. In other words, this section discusses how to setup a client that uses a service that authenticates with CAS. The next section describes how to setup a stateless service to Authenticate using CAS.
Configuring CAS to Obtain Proxy Granting Tickets
In order to authenticate to a stateless service, the application needs to obtain a proxy granting ticket (PGT). This section describes how to configure Spring Security to obtain a PGT building upon thencas-st[Service Ticket Authentication] configuration.
The first step is to include a ProxyGrantingTicketStorage
in your Spring Security configuration.
This is used to store PGT’s that are obtained by the CasAuthenticationFilter
so that they can be used to obtain proxy tickets.
An example configuration is shown below
<!--
NOTE: In a real application you should not use an in memory implementation.
You will also want to ensure to clean up expired tickets by calling
ProxyGrantingTicketStorage.cleanup()
-->
<bean id="pgtStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>
The next step is to update the CasAuthenticationProvider
to be able to obtain proxy tickets.
To do this replace the Cas20ServiceTicketValidator
with a Cas20ProxyTicketValidator
.
The proxyCallbackUrl
should be set to a URL that the application will receive PGT’s at.
Last, the configuration should also reference the ProxyGrantingTicketStorage
so it can use a PGT to obtain proxy tickets.
You can find an example of the configuration changes that should be made below.
<bean id="casAuthenticationProvider"
class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator">
<constructor-arg value="https://localhost:9443/cas"/>
<property name="proxyCallbackUrl"
value="https://localhost:8443/cas-sample/login/cas/proxyreceptor"/>
<property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
</bean>
</property>
</bean>
The last step is to update the CasAuthenticationFilter
to accept PGT and to store them in the ProxyGrantingTicketStorage
.
It is important the proxyReceptorUrl
matches the proxyCallbackUrl
of the Cas20ProxyTicketValidator
.
An example configuration is shown below.
<bean id="casFilter"
class="org.springframework.security.cas.web.CasAuthenticationFilter">
...
<property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
<property name="proxyReceptorUrl" value="/login/cas/proxyreceptor"/>
</bean>
Calling a Stateless Service Using a Proxy Ticket
Now that Spring Security obtains PGTs, you can use them to create proxy tickets which can be used to authenticate to a stateless service.
The CAS sample application contains a working example in the ProxyTicketSampleServlet
.
Example code can be found below:
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// NOTE: The CasAuthenticationToken can also be obtained using
// SecurityContextHolder.getContext().getAuthentication()
final CasAuthenticationToken token = (CasAuthenticationToken) request.getUserPrincipal();
// proxyTicket could be reused to make calls to the CAS service even if the
// target url differs
final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl);
// Make a remote call using the proxy ticket
final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8");
String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8");
...
}
protected fun doGet(request: HttpServletRequest, response: HttpServletResponse?) {
// NOTE: The CasAuthenticationToken can also be obtained using
// SecurityContextHolder.getContext().getAuthentication()
val token = request.userPrincipal as CasAuthenticationToken
// proxyTicket could be reused to make calls to the CAS service even if the
// target url differs
val proxyTicket = token.assertion.principal.getProxyTicketFor(targetUrl)
// Make a remote call using the proxy ticket
val serviceUrl: String = targetUrl + "?ticket=" + URLEncoder.encode(proxyTicket, "UTF-8")
val proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8")
}
Proxy Ticket Authentication
The CasAuthenticationProvider
distinguishes between stateful and stateless clients.
A stateful client is considered any that submits to the filterProcessUrl
of the CasAuthenticationFilter
.
A stateless client is any that presents an authentication request to CasAuthenticationFilter
on a URL other than the filterProcessUrl
.
Because remoting protocols have no way of presenting themselves within the context of an HttpSession
, it isn’t possible to rely on the default practice of storing the security context in the session between requests.
Furthermore, because the CAS server invalidates a ticket after it has been validated by the TicketValidator
, presenting the same proxy ticket on subsequent requests will not work.
One obvious option is to not use CAS at all for remoting protocol clients.
However, this would eliminate many of the desirable features of CAS.
As a middle-ground, the CasAuthenticationProvider
uses a StatelessTicketCache
.
This is used solely for stateless clients which use a principal equal to CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER
.
What happens is the CasAuthenticationProvider
will store the resulting CasAuthenticationToken
in the StatelessTicketCache
, keyed on the proxy ticket.
Accordingly, remoting protocol clients can present the same proxy ticket and the CasAuthenticationProvider
will not need to contact the CAS server for validation (aside from the first request).
Once authenticated, the proxy ticket could be used for URLs other than the original target service.
This section builds upon the previous sections to accommodate proxy ticket authentication. The first step is to specify to authenticate all artifacts as shown below.
<bean id="serviceProperties"
class="org.springframework.security.cas.ServiceProperties">
...
<property name="authenticateAllArtifacts" value="true"/>
</bean>
The next step is to specify serviceProperties
and the authenticationDetailsSource
for the CasAuthenticationFilter
.
The serviceProperties
property instructs the CasAuthenticationFilter
to attempt to authenticate all artifacts instead of only ones present on the filterProcessUrl
.
The ServiceAuthenticationDetailsSource
creates a ServiceAuthenticationDetails
that ensures the current URL, based upon the HttpServletRequest
, is used as the service URL when validating the ticket.
The method for generating the service URL can be customized by injecting a custom AuthenticationDetailsSource
that returns a custom ServiceAuthenticationDetails
.
<bean id="casFilter"
class="org.springframework.security.cas.web.CasAuthenticationFilter">
...
<property name="serviceProperties" ref="serviceProperties"/>
<property name="authenticationDetailsSource">
<bean class=
"org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource">
<constructor-arg ref="serviceProperties"/>
</bean>
</property>
</bean>
You will also need to update the CasAuthenticationProvider
to handle proxy tickets.
To do this replace the Cas20ServiceTicketValidator
with a Cas20ProxyTicketValidator
.
You will need to configure the statelessTicketCache
and which proxies you want to accept.
You can find an example of the updates required to accept all proxies below.
<bean id="casAuthenticationProvider"
class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator">
<constructor-arg value="https://localhost:9443/cas"/>
<property name="acceptAnyProxy" value="true"/>
</bean>
</property>
<property name="statelessTicketCache">
<bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache">
<property name="cache">
<bean class="net.sf.ehcache.Cache"
init-method="initialise" destroy-method="dispose">
<constructor-arg value="casTickets"/>
<constructor-arg value="50"/>
<constructor-arg value="true"/>
<constructor-arg value="false"/>
<constructor-arg value="3600"/>
<constructor-arg value="900"/>
</bean>
</property>
</bean>
</property>
</bean>
10.18. X.509 Authentication
10.18.1. Overview
The most common use of X.509 certificate authentication is in verifying the identity of a server when using SSL, most commonly when using HTTPS from a browser. The browser will automatically check that the certificate presented by a server has been issued (ie digitally signed) by one of a list of trusted certificate authorities which it maintains.
You can also use SSL with "mutual authentication"; the server will then request a valid certificate from the client as part of the SSL handshake. The server will authenticate the client by checking that its certificate is signed by an acceptable authority. If a valid certificate has been provided, it can be obtained through the servlet API in an application. Spring Security X.509 module extracts the certificate using a filter. It maps the certificate to an application user and loads that user’s set of granted authorities for use with the standard Spring Security infrastructure.
You should be familiar with using certificates and setting up client authentication for your servlet container before attempting to use it with Spring Security. Most of the work is in creating and installing suitable certificates and keys. For example, if you’re using Tomcat then read the instructions here https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html. It’s important that you get this working before trying it out with Spring Security
10.18.2. Adding X.509 Authentication to Your Web Application
Enabling X.509 client authentication is very straightforward.
Just add the <x509/>
element to your http security namespace configuration.
<http>
...
<x509 subject-principal-regex="CN=(.*?)," user-service-ref="userService"/>;
</http>
The element has two optional attributes:
-
subject-principal-regex
. The regular expression used to extract a username from the certificate’s subject name. The default value is shown above. This is the username which will be passed to theUserDetailsService
to load the authorities for the user. -
user-service-ref
. This is the bean Id of theUserDetailsService
to be used with X.509. It isn’t needed if there is only one defined in your application context.
The subject-principal-regex
should contain a single group.
For example the default expression "CN=(.*?)," matches the common name field.
So if the subject name in the certificate is "CN=Jimi Hendrix, OU=…", this will give a user name of "Jimi Hendrix".
The matches are case insensitive.
So "emailAddress=(.*?)," will match "EMAILADDRESS=[email protected],CN=…" giving a user name "[email protected]".
If the client presents a certificate and a valid username is successfully extracted, then there should be a valid Authentication
object in the security context.
If no certificate is found, or no corresponding user could be found then the security context will remain empty.
This means that you can easily use X.509 authentication with other options such as a form-based login.
10.18.3. Setting up SSL in Tomcat
There are some pre-generated certificates in the Spring Security Samples repository.
You can use these to enable SSL for testing if you don’t want to generate your own.
The file server.jks
contains the server certificate, private key and the issuing certificate authority certificate.
There are also some client certificate files for the users from the sample applications.
You can install these in your browser to enable SSL client authentication.
To run tomcat with SSL support, drop the server.jks
file into the tomcat conf
directory and add the following connector to the server.xml
file
<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" scheme="https" secure="true"
clientAuth="true" sslProtocol="TLS"
keystoreFile="${catalina.home}/conf/server.jks"
keystoreType="JKS" keystorePass="password"
truststoreFile="${catalina.home}/conf/server.jks"
truststoreType="JKS" truststorePass="password"
/>
clientAuth
can also be set to want
if you still want SSL connections to succeed even if the client doesn’t provide a certificate.
Clients which don’t present a certificate won’t be able to access any objects secured by Spring Security unless you use a non-X.509 authentication mechanism, such as form authentication.
10.19. Run-As Authentication Replacement
10.19.1. Overview
The AbstractSecurityInterceptor
is able to temporarily replace the Authentication
object in the SecurityContext
and SecurityContextHolder
during the secure object callback phase.
This only occurs if the original Authentication
object was successfully processed by the AuthenticationManager
and AccessDecisionManager
.
The RunAsManager
will indicate the replacement Authentication
object, if any, that should be used during the SecurityInterceptorCallback
.
By temporarily replacing the Authentication
object during the secure object callback phase, the secured invocation will be able to call other objects which require different authentication and authorization credentials.
It will also be able to perform any internal security checks for specific GrantedAuthority
objects.
Because Spring Security provides a number of helper classes that automatically configure remoting protocols based on the contents of the SecurityContextHolder
, these run-as replacements are particularly useful when calling remote web services
10.19.2. Configuration
A RunAsManager
interface is provided by Spring Security:
Authentication buildRunAs(Authentication authentication, Object object,
List<ConfigAttribute> config);
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
The first method returns the Authentication
object that should replace the existing Authentication
object for the duration of the method invocation.
If the method returns null
, it indicates no replacement should be made.
The second method is used by the AbstractSecurityInterceptor
as part of its startup validation of configuration attributes.
The supports(Class)
method is called by a security interceptor implementation to ensure the configured RunAsManager
supports the type of secure object that the security interceptor will present.
One concrete implementation of a RunAsManager
is provided with Spring Security.
The RunAsManagerImpl
class returns a replacement RunAsUserToken
if any ConfigAttribute
starts with RUN_AS_
.
If any such ConfigAttribute
is found, the replacement RunAsUserToken
will contain the same principal, credentials and granted authorities as the original Authentication
object, along with a new SimpleGrantedAuthority
for each RUN_AS_
ConfigAttribute
.
Each new SimpleGrantedAuthority
will be prefixed with ROLE_
, followed by the RUN_AS
ConfigAttribute
.
For example, a RUN_AS_SERVER
will result in the replacement RunAsUserToken
containing a ROLE_RUN_AS_SERVER
granted authority.
The replacement RunAsUserToken
is just like any other Authentication
object.
It needs to be authenticated by the AuthenticationManager
, probably via delegation to a suitable AuthenticationProvider
.
The RunAsImplAuthenticationProvider
performs such authentication.
It simply accepts as valid any RunAsUserToken
presented.
To ensure malicious code does not create a RunAsUserToken
and present it for guaranteed acceptance by the RunAsImplAuthenticationProvider
, the hash of a key is stored in all generated tokens.
The RunAsManagerImpl
and RunAsImplAuthenticationProvider
is created in the bean context with the same key:
<bean id="runAsManager"
class="org.springframework.security.access.intercept.RunAsManagerImpl">
<property name="key" value="my_run_as_password"/>
</bean>
<bean id="runAsAuthenticationProvider"
class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
<property name="key" value="my_run_as_password"/>
</bean>
By using the same key, each RunAsUserToken
can be validated it was created by an approved RunAsManagerImpl
.
The RunAsUserToken
is immutable after creation for security reasons
10.20. Handling Logouts
10.20.1. Logout Java/Kotlin Configuration
When using the WebSecurityConfigurerAdapter
, logout capabilities are automatically applied.
The default is that accessing the URL /logout
will log the user out by:
-
Invalidating the HTTP Session
-
Cleaning up any RememberMe authentication that was configured
-
Clearing the
SecurityContextHolder
-
Redirect to
/login?logout
Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:
protected void configure(HttpSecurity http) throws Exception {
http
.logout(logout -> logout (1)
.logoutUrl("/my/logout") (2)
.logoutSuccessUrl("/my/index") (3)
.logoutSuccessHandler(logoutSuccessHandler) (4)
.invalidateHttpSession(true) (5)
.addLogoutHandler(logoutHandler) (6)
.deleteCookies(cookieNamesToClear) (7)
)
...
}
override fun configure(http: HttpSecurity) {
http {
logout {
logoutUrl = "/my/logout" (1)
logoutSuccessUrl = "/my/index" (2)
logoutSuccessHandler = customLogoutSuccessHandler (3)
invalidateHttpSession = true (4)
addLogoutHandler(logoutHandler) (5)
deleteCookies(cookieNamesToClear) (6)
}
}
}
1 | Provides logout support.
This is automatically applied when using WebSecurityConfigurerAdapter . |
2 | The URL that triggers log out to occur (default is /logout ).
If CSRF protection is enabled (default), then the request must also be a POST.
For more information, please consult the JavaDoc. |
3 | The URL to redirect to after logout has occurred.
The default is /login?logout .
For more information, please consult the JavaDoc. |
4 | Let’s you specify a custom LogoutSuccessHandler .
If this is specified, logoutSuccessUrl() is ignored.
For more information, please consult the JavaDoc. |
5 | Specify whether to invalidate the HttpSession at the time of logout.
This is true by default.
Configures the SecurityContextLogoutHandler under the covers.
For more information, please consult the JavaDoc. |
6 | Adds a LogoutHandler .
SecurityContextLogoutHandler is added as the last LogoutHandler by default. |
7 | Allows specifying the names of cookies to be removed on logout success.
This is a shortcut for adding a CookieClearingLogoutHandler explicitly. |
Logouts can of course also be configured using the XML Namespace notation. Please see the documentation for the logout element in the Spring Security XML Namespace section for further details. |
Generally, in order to customize logout functionality, you can add
LogoutHandler
and/or
LogoutSuccessHandler
implementations.
For many common scenarios, these handlers are applied under the
covers when using the fluent API.
10.20.2. Logout XML Configuration
The logout
element adds support for logging out by navigating to a particular URL.
The default logout URL is /logout
, but you can set it to something else using the logout-url
attribute.
More information on other available attributes may be found in the namespace appendix.
10.20.3. LogoutHandler
Generally, LogoutHandler
implementations indicate classes that are able to participate in logout handling.
They are expected to be invoked to perform necessary clean-up.
As such they should
not throw exceptions.
Various implementations are provided:
Please see Remember-Me Interfaces and Implementations for details.
Instead of providing LogoutHandler
implementations directly, the fluent API also provides shortcuts that provide the respective LogoutHandler
implementations under the covers.
E.g. deleteCookies()
allows specifying the names of one or more cookies to be removed on logout success.
This is a shortcut compared to adding a CookieClearingLogoutHandler
.
10.20.4. LogoutSuccessHandler
The LogoutSuccessHandler
is called after a successful logout by the LogoutFilter
, to handle e.g.
redirection or forwarding to the appropriate destination.
Note that the interface is almost the same as the LogoutHandler
but may raise an exception.
The following implementations are provided:
-
HttpStatusReturningLogoutSuccessHandler
As mentioned above, you don’t need to specify the SimpleUrlLogoutSuccessHandler
directly.
Instead, the fluent API provides a shortcut by setting the logoutSuccessUrl()
.
This will setup the SimpleUrlLogoutSuccessHandler
under the covers.
The provided URL will be redirected to after a logout has occurred.
The default is /login?logout
.
The HttpStatusReturningLogoutSuccessHandler
can be interesting in REST API type scenarios.
Instead of redirecting to a URL upon the successful logout, this LogoutSuccessHandler
allows you to provide a plain HTTP status code to be returned.
If not configured a status code 200 will be returned by default.
10.20.5. Further Logout-Related References
-
Logging Out in section CSRF Caveats
-
Section Single Logout (CAS protocol)
-
Documentation for the logout element in the Spring Security XML Namespace section
10.21. Authentication Events
For each authentication that succeeds or fails, a AuthenticationSuccessEvent
or AbstractAuthenticationFailureEvent
is fired, respectively.
To listen for these events, you must first publish an AuthenticationEventPublisher
.
Spring Security’s DefaultAuthenticationEventPublisher
will probably do fine:
@Bean
public AuthenticationEventPublisher authenticationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}
@Bean
fun authenticationEventPublisher
(applicationEventPublisher: ApplicationEventPublisher?): AuthenticationEventPublisher {
return DefaultAuthenticationEventPublisher(applicationEventPublisher)
}
Then, you can use Spring’s @EventListener
support:
@Component
public class AuthenticationEvents {
@EventListener
public void onSuccess(AuthenticationSuccessEvent success) {
// ...
}
@EventListener
public void onFailure(AbstractAuthenticationFailureEvent failures) {
// ...
}
}
@Component
class AuthenticationEvents {
@EventListener
fun onSuccess(success: AuthenticationSuccessEvent?) {
// ...
}
@EventListener
fun onFailure(failures: AbstractAuthenticationFailureEvent?) {
// ...
}
}
While similar to AuthenticationSuccessHandler
and AuthenticationFailureHandler
, these are nice in that they can be used independently from the servlet API.
10.21.1. Adding Exception Mappings
DefaultAuthenticationEventPublisher
by default will publish an AbstractAuthenticationFailureEvent
for the following events:
Exception |
Event |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The publisher does an exact Exception
match, which means that sub-classes of these exceptions won’t also produce events.
To that end, you may want to supply additional mappings to the publisher via the setAdditionalExceptionMappings
method:
@Bean
public AuthenticationEventPublisher authenticationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
Map<Class<? extends AuthenticationException>,
Class<? extends AbstractAuthenticationFailureEvent>> mapping =
Collections.singletonMap(FooException.class, FooEvent.class);
AuthenticationEventPublisher authenticationEventPublisher =
new DefaultAuthenticationEventPublisher(applicationEventPublisher);
authenticationEventPublisher.setAdditionalExceptionMappings(mapping);
return authenticationEventPublisher;
}
@Bean
fun authenticationEventPublisher
(applicationEventPublisher: ApplicationEventPublisher?): AuthenticationEventPublisher {
val mapping: Map<Class<out AuthenticationException>, Class<out AbstractAuthenticationFailureEvent>> =
mapOf(Pair(FooException::class.java, FooEvent::class.java))
val authenticationEventPublisher = DefaultAuthenticationEventPublisher(applicationEventPublisher)
authenticationEventPublisher.setAdditionalExceptionMappings(mapping)
return authenticationEventPublisher
}
10.21.2. Default Event
And, you can supply a catch-all event to fire in the case of any AuthenticationException
:
@Bean
public AuthenticationEventPublisher authenticationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
AuthenticationEventPublisher authenticationEventPublisher =
new DefaultAuthenticationEventPublisher(applicationEventPublisher);
authenticationEventPublisher.setDefaultAuthenticationFailureEvent
(GenericAuthenticationFailureEvent.class);
return authenticationEventPublisher;
}
@Bean
fun authenticationEventPublisher
(applicationEventPublisher: ApplicationEventPublisher?): AuthenticationEventPublisher {
val authenticationEventPublisher = DefaultAuthenticationEventPublisher(applicationEventPublisher)
authenticationEventPublisher.setDefaultAuthenticationFailureEvent(GenericAuthenticationFailureEvent::class.java)
return authenticationEventPublisher
}
11. Authorization
The advanced authorization capabilities within Spring Security represent one of the most compelling reasons for its popularity. Irrespective of how you choose to authenticate - whether using a Spring Security-provided mechanism and provider, or integrating with a container or other non-Spring Security authentication authority - you will find the authorization services can be used within your application in a consistent and simple way.
In this part we’ll explore the different AbstractSecurityInterceptor
implementations, which were introduced in Part I.
We then move on to explore how to fine-tune authorization through use of domain access control lists.
11.1. Authorization Architecture
11.1.1. Authorities
Authentication
, discusses how all Authentication
implementations store a list of GrantedAuthority
objects.
These represent the authorities that have been granted to the principal.
the GrantedAuthority
objects are inserted into the Authentication
object by the AuthenticationManager
and are later read by AccessDecisionManager
s when making authorization decisions.
GrantedAuthority
is an interface with only one method:
String getAuthority();
This method allows
AccessDecisionManager
s to obtain a precise String
representation of the GrantedAuthority
.
By returning a representation as a String
, a GrantedAuthority
can be easily "read" by most AccessDecisionManager
s.
If a GrantedAuthority
cannot be precisely represented as a String
, the GrantedAuthority
is considered "complex" and getAuthority()
must return null
.
An example of a "complex" GrantedAuthority
would be an implementation that stores a list of operations and authority thresholds that apply to different customer account numbers.
Representing this complex GrantedAuthority
as a String
would be quite difficult, and as a result the getAuthority()
method should return null
.
This will indicate to any AccessDecisionManager
that it will need to specifically support the GrantedAuthority
implementation in order to understand its contents.
Spring Security includes one concrete GrantedAuthority
implementation, SimpleGrantedAuthority
.
This allows any user-specified String
to be converted into a GrantedAuthority
.
All AuthenticationProvider
s included with the security architecture use SimpleGrantedAuthority
to populate the Authentication
object.
11.1.2. Pre-Invocation Handling
Spring Security provides interceptors which control access to secure objects such as method invocations or web requests.
A pre-invocation decision on whether the invocation is allowed to proceed is made by the AccessDecisionManager
.
The AccessDecisionManager
The AccessDecisionManager
is called by the AbstractSecurityInterceptor
and is responsible for making final access control decisions.
The AccessDecisionManager
interface contains three methods:
void decide(Authentication authentication, Object secureObject,
Collection<ConfigAttribute> attrs) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
The AccessDecisionManager
's decide
method is passed all the relevant information it needs in order to make an authorization decision.
In particular, passing the secure Object
enables those arguments contained in the actual secure object invocation to be inspected.
For example, let’s assume the secure object was a MethodInvocation
.
It would be easy to query the MethodInvocation
for any Customer
argument, and then implement some sort of security logic in the AccessDecisionManager
to ensure the principal is permitted to operate on that customer.
Implementations are expected to throw an AccessDeniedException
if access is denied.
The supports(ConfigAttribute)
method is called by the AbstractSecurityInterceptor
at startup time to determine if the AccessDecisionManager
can process the passed ConfigAttribute
.
The supports(Class)
method is called by a security interceptor implementation to ensure the configured AccessDecisionManager
supports the type of secure object that the security interceptor will present.
Voting-Based AccessDecisionManager Implementations
Whilst users can implement their own AccessDecisionManager
to control all aspects of authorization, Spring Security includes several AccessDecisionManager
implementations that are based on voting.
Voting Decision Manager illustrates the relevant classes.
Using this approach, a series of AccessDecisionVoter
implementations are polled on an authorization decision.
The AccessDecisionManager
then decides whether or not to throw an AccessDeniedException
based on its assessment of the votes.
The AccessDecisionVoter
interface has three methods:
int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
Concrete implementations return an int
, with possible values being reflected in the AccessDecisionVoter
static fields ACCESS_ABSTAIN
, ACCESS_DENIED
and ACCESS_GRANTED
.
A voting implementation will return ACCESS_ABSTAIN
if it has no opinion on an authorization decision.
If it does have an opinion, it must return either ACCESS_DENIED
or ACCESS_GRANTED
.
There are three concrete AccessDecisionManager
s provided with Spring Security that tally the votes.
The ConsensusBased
implementation will grant or deny access based on the consensus of non-abstain votes.
Properties are provided to control behavior in the event of an equality of votes or if all votes are abstain.
The AffirmativeBased
implementation will grant access if one or more ACCESS_GRANTED
votes were received (i.e. a deny vote will be ignored, provided there was at least one grant vote).
Like the ConsensusBased
implementation, there is a parameter that controls the behavior if all voters abstain.
The UnanimousBased
provider expects unanimous ACCESS_GRANTED
votes in order to grant access, ignoring abstains.
It will deny access if there is any ACCESS_DENIED
vote.
Like the other implementations, there is a parameter that controls the behaviour if all voters abstain.
It is possible to implement a custom AccessDecisionManager
that tallies votes differently.
For example, votes from a particular AccessDecisionVoter
might receive additional weighting, whilst a deny vote from a particular voter may have a veto effect.
RoleVoter
The most commonly used AccessDecisionVoter
provided with Spring Security is the simple RoleVoter
, which treats configuration attributes as simple role names and votes to grant access if the user has been assigned that role.
It will vote if any ConfigAttribute
begins with the prefix ROLE_
.
It will vote to grant access if there is a GrantedAuthority
which returns a String
representation (via the getAuthority()
method) exactly equal to one or more ConfigAttributes
starting with the prefix ROLE_
.
If there is no exact match of any ConfigAttribute
starting with ROLE_
, the RoleVoter
will vote to deny access.
If no ConfigAttribute
begins with ROLE_
, the voter will abstain.
AuthenticatedVoter
Another voter which we’ve implicitly seen is the AuthenticatedVoter
, which can be used to differentiate between anonymous, fully-authenticated and remember-me authenticated users.
Many sites allow certain limited access under remember-me authentication, but require a user to confirm their identity by logging in for full access.
When we’ve used the attribute IS_AUTHENTICATED_ANONYMOUSLY
to grant anonymous access, this attribute was being processed by the AuthenticatedVoter
.
See the Javadoc for this class for more information.
Custom Voters
Obviously, you can also implement a custom AccessDecisionVoter
and you can put just about any access-control logic you want in it.
It might be specific to your application (business-logic related) or it might implement some security administration logic.
For example, you’ll find a blog article on the Spring web site which describes how to use a voter to deny access in real-time to users whose accounts have been suspended.
11.1.3. After Invocation Handling
Whilst the AccessDecisionManager
is called by the AbstractSecurityInterceptor
before proceeding with the secure object invocation, some applications need a way of modifying the object actually returned by the secure object invocation.
Whilst you could easily implement your own AOP concern to achieve this, Spring Security provides a convenient hook that has several concrete implementations that integrate with its ACL capabilities.
After Invocation Implementation illustrates Spring Security’s AfterInvocationManager
and its concrete implementations.
Like many other parts of Spring Security, AfterInvocationManager
has a single concrete implementation, AfterInvocationProviderManager
, which polls a list of AfterInvocationProvider
s.
Each AfterInvocationProvider
is allowed to modify the return object or throw an AccessDeniedException
.
Indeed multiple providers can modify the object, as the result of the previous provider is passed to the next in the list.
Please be aware that if you’re using AfterInvocationManager
, you will still need configuration attributes that allow the MethodSecurityInterceptor
's AccessDecisionManager
to allow an operation.
If you’re using the typical Spring Security included AccessDecisionManager
implementations, having no configuration attributes defined for a particular secure method invocation will cause each AccessDecisionVoter
to abstain from voting.
In turn, if the AccessDecisionManager
property “allowIfAllAbstainDecisions” is false
, an AccessDeniedException
will be thrown.
You may avoid this potential issue by either (i) setting “allowIfAllAbstainDecisions” to true
(although this is generally not recommended) or (ii) simply ensure that there is at least one configuration attribute that an AccessDecisionVoter
will vote to grant access for.
This latter (recommended) approach is usually achieved through a ROLE_USER
or ROLE_AUTHENTICATED
configuration attribute.
11.1.4. Hierarchical Roles
It is a common requirement that a particular role in an application should automatically "include" other roles. For example, in an application which has the concept of an "admin" and a "user" role, you may want an admin to be able to do everything a normal user can. To achieve this, you can either make sure that all admin users are also assigned the "user" role. Alternatively, you can modify every access constraint which requires the "user" role to also include the "admin" role. This can get quite complicated if you have a lot of different roles in your application.
The use of a role-hierarchy allows you to configure which roles (or authorities) should include others.
An extended version of Spring Security’s RoleVoter, RoleHierarchyVoter
, is configured with a RoleHierarchy
, from which it obtains all the "reachable authorities" which the user is assigned.
A typical configuration might look like this:
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
<constructor-arg ref="roleHierarchy" />
</bean>
<bean id="roleHierarchy"
class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
<property name="hierarchy">
<value>
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST
</value>
</property>
</bean>
Here we have four roles in a hierarchy ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST
.
A user who is authenticated with ROLE_ADMIN
, will behave as if they have all four roles when security constraints are evaluated against an AccessDecisionManager
configured with the above RoleHierarchyVoter
.
The >
symbol can be thought of as meaning "includes".
Role hierarchies offer a convenient means of simplifying the access-control configuration data for your application and/or reducing the number of authorities which you need to assign to a user. For more complex requirements you may wish to define a logical mapping between the specific access-rights your application requires and the roles that are assigned to users, translating between the two when loading the user information.
11.2. Authorize HttpServletRequest with FilterSecurityInterceptor
This section builds on Servlet Architecture and Implementation by digging deeper into how authorization works within Servlet based applications.
The FilterSecurityInterceptor
provides authorization for HttpServletRequest
s.
It is inserted into the FilterChainProxy as one of the Security Filters.
-
First, the
FilterSecurityInterceptor
obtains an Authentication from the SecurityContextHolder. -
Second,
FilterSecurityInterceptor
creates aFilterInvocation
from theHttpServletRequest
,HttpServletResponse
, andFilterChain
that are passed into theFilterSecurityInterceptor
. -
Next, it passes the
FilterInvocation
toSecurityMetadataSource
to get theConfigAttribute
s. -
Finally, it passes the
Authentication
,FilterInvocation
, andConfigAttribute
s to theAccessDecisionManager
.-
If authorization is denied, an
AccessDeniedException
is thrown. In this case theExceptionTranslationFilter
handles theAccessDeniedException
. -
If access is granted,
FilterSecurityInterceptor
continues with the FilterChain which allows the application to process normally.
-
By default, Spring Security’s authorization will require all requests to be authenticated. The explicit configuration looks like:
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
);
}
<http>
<!-- ... -->
<intercept-url pattern="/**" access="authenticated"/>
</http>
fun configure(http: HttpSecurity) {
http {
// ...
authorizeRequests {
authorize(anyRequest, authenticated)
}
}
}
We can configure Spring Security to have different rules by adding more rules in order of precedence.
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize (1)
.mvcMatchers("/resources/**", "/signup", "/about").permitAll() (2)
.mvcMatchers("/admin/**").hasRole("ADMIN") (3)
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") (4)
.anyRequest().denyAll() (5)
);
}
<http> (1)
<!-- ... -->
(2)
<intercept-url pattern="/resources/**" access="permitAll"/>
<intercept-url pattern="/signup" access="permitAll"/>
<intercept-url pattern="/about" access="permitAll"/>
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/> (3)
<intercept-url pattern="/db/**" access="hasRole('ADMIN') and hasRole('DBA')"/> (4)
<intercept-url pattern="/**" access="denyAll"/> (5)
</http>
fun configure(http: HttpSecurity) {
http {
authorizeRequests { (1)
authorize("/resources/**", permitAll) (2)
authorize("/signup", permitAll)
authorize("/about", permitAll)
authorize("/admin/**", hasRole("ADMIN")) (3)
authorize("/db/**", "hasRole('ADMIN') and hasRole('DBA')") (4)
authorize(anyRequest, denyAll) (5)
}
}
}
1 | There are multiple authorization rules specified. Each rule is considered in the order they were declared. |
2 | We specified multiple URL patterns that any user can access. Specifically, any user can access a request if the URL starts with "/resources/", equals "/signup", or equals "/about". |
3 | Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN".
You will notice that since we are invoking the hasRole method we do not need to specify the "ROLE_" prefix. |
4 | Any URL that starts with "/db/" requires the user to have both "ROLE_ADMIN" and "ROLE_DBA".
You will notice that since we are using the hasRole expression we do not need to specify the "ROLE_" prefix. |
5 | Any URL that has not already been matched on is denied access. This is a good strategy if you do not want to accidentally forget to update your authorization rules. |
11.3. Expression-Based Access Control
Spring Security 3.0 introduced the ability to use Spring EL expressions as an authorization mechanism in addition to the simple use of configuration attributes and access-decision voters which have seen before. Expression-based access control is built on the same architecture but allows complicated Boolean logic to be encapsulated in a single expression.
11.3.1. Overview
Spring Security uses Spring EL for expression support and you should look at how that works if you are interested in understanding the topic in more depth. Expressions are evaluated with a "root object" as part of the evaluation context. Spring Security uses specific classes for web and method security as the root object, in order to provide built-in expressions and access to values such as the current principal.
Common Built-In Expressions
The base class for expression root objects is SecurityExpressionRoot
.
This provides some common expressions which are available in both web and method security.
Expression | Description |
---|---|
|
Returns For example, By default if the supplied role does not start with 'ROLE_' it will be added.
This can be customized by modifying the |
|
Returns For example, By default if the supplied role does not start with 'ROLE_' it will be added.
This can be customized by modifying the |
|
Returns For example, |
|
Returns For example, |
|
Allows direct access to the principal object representing the current user |
|
Allows direct access to the current |
|
Always evaluates to |
|
Always evaluates to |
|
Returns |
|
Returns |
|
Returns |
|
Returns |
|
Returns |
|
Returns |
11.3.2. Web Security Expressions
To use expressions to secure individual URLs, you would first need to set the use-expressions
attribute in the <http>
element to true
.
Spring Security will then expect the access
attributes of the <intercept-url>
elements to contain Spring EL expressions.
The expressions should evaluate to a Boolean, defining whether access should be allowed or not.
For example:
<http>
<intercept-url pattern="/admin*"
access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
...
</http>
Here we have defined that the "admin" area of an application (defined by the URL pattern) should only be available to users who have the granted authority "admin" and whose IP address matches a local subnet.
We’ve already seen the built-in hasRole
expression in the previous section.
The expression hasIpAddress
is an additional built-in expression which is specific to web security.
It is defined by the WebSecurityExpressionRoot
class, an instance of which is used as the expression root object when evaluating web-access expressions.
This object also directly exposed the HttpServletRequest
object under the name request
so you can invoke the request directly in an expression.
If expressions are being used, a WebExpressionVoter
will be added to the AccessDecisionManager
which is used by the namespace.
So if you aren’t using the namespace and want to use expressions, you will have to add one of these to your configuration.
Referring to Beans in Web Security Expressions
If you wish to extend the expressions that are available, you can easily refer to any Spring Bean you expose.
For example, assuming you have a Bean with the name of webSecurity
that contains the following method signature:
public class WebSecurity {
public boolean check(Authentication authentication, HttpServletRequest request) {
...
}
}
class WebSecurity {
fun check(authentication: Authentication?, request: HttpServletRequest?): Boolean {
// ...
}
}
You could refer to the method using:
http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
...
)
<http>
<intercept-url pattern="/user/**"
access="@webSecurity.check(authentication,request)"/>
...
</http>
http {
authorizeRequests {
authorize("/user/**", "@webSecurity.check(authentication,request)")
}
}
Path Variables in Web Security Expressions
At times it is nice to be able to refer to path variables within a URL.
For example, consider a RESTful application that looks up a user by id from the URL path in the format /user/{userId}
.
You can easily refer to the path variable by placing it in the pattern.
For example, if you had a Bean with the name of webSecurity
that contains the following method signature:
public class WebSecurity {
public boolean checkUserId(Authentication authentication, int id) {
...
}
}
class WebSecurity {
fun checkUserId(authentication: Authentication?, id: Int): Boolean {
// ...
}
}
You could refer to the method using:
http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
...
);
<http>
<intercept-url pattern="/user/{userId}/**"
access="@webSecurity.checkUserId(authentication,#userId)"/>
...
</http>
http {
authorizeRequests {
authorize("/user/{userId}/**", "@webSecurity.checkUserId(authentication,#userId)")
}
}
In this configuration URLs that match would pass in the path variable (and convert it) into checkUserId method.
For example, if the URL were /user/123/resource
, then the id passed in would be 123
.
11.3.3. Method Security Expressions
Method security is a bit more complicated than a simple allow or deny rule. Spring Security 3.0 introduced some new annotations in order to allow comprehensive support for the use of expressions.
@Pre and @Post Annotations
There are four annotations which support expression attributes to allow pre and post-invocation authorization checks and also to support filtering of submitted collection arguments or return values.
They are @PreAuthorize
, @PreFilter
, @PostAuthorize
and @PostFilter
.
Their use is enabled through the global-method-security
namespace element:
<global-method-security pre-post-annotations="enabled"/>
Access Control using @PreAuthorize and @PostAuthorize
The most obviously useful annotation is @PreAuthorize
which decides whether a method can actually be invoked or not.
For example (from the "Contacts" sample application)
@PreAuthorize("hasRole('USER')")
public void create(Contact contact);
@PreAuthorize("hasRole('USER')")
fun create(contact: Contact?)
which means that access will only be allowed for users with the role "ROLE_USER". Obviously the same thing could easily be achieved using a traditional configuration and a simple configuration attribute for the required role. But what about:
@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);
@PreAuthorize("hasPermission(#contact, 'admin')")
fun deletePermission(contact: Contact?, recipient: Sid?, permission: Permission?)
Here we’re actually using a method argument as part of the expression to decide whether the current user has the "admin"permission for the given contact.
The built-in hasPermission()
expression is linked into the Spring Security ACL module through the application context, as we’ll see below.
You can access any of the method arguments by name as expression variables.
There are a number of ways in which Spring Security can resolve the method arguments.
Spring Security uses DefaultSecurityParameterNameDiscoverer
to discover the parameter names.
By default, the following options are tried for a method as a whole.
-
If Spring Security’s
@P
annotation is present on a single argument to the method, the value will be used. This is useful for interfaces compiled with a JDK prior to JDK 8 which do not contain any information about the parameter names. For example:Javaimport org.springframework.security.access.method.P; ... @PreAuthorize("#c.name == authentication.name") public void doSomething(@P("c") Contact contact);
Kotlinimport org.springframework.security.access.method.P ... @PreAuthorize("#c.name == authentication.name") fun doSomething(@P("c") contact: Contact?)
Behind the scenes this is implemented using
AnnotationParameterNameDiscoverer
which can be customized to support the value attribute of any specified annotation. -
If Spring Data’s
@Param
annotation is present on at least one parameter for the method, the value will be used. This is useful for interfaces compiled with a JDK prior to JDK 8 which do not contain any information about the parameter names. For example:Javaimport org.springframework.data.repository.query.Param; ... @PreAuthorize("#n == authentication.name") Contact findContactByName(@Param("n") String name);
Kotlinimport org.springframework.data.repository.query.Param ... @PreAuthorize("#n == authentication.name") fun findContactByName(@Param("n") name: String?): Contact?
Behind the scenes this is implemented using
AnnotationParameterNameDiscoverer
which can be customized to support the value attribute of any specified annotation. -
If JDK 8 was used to compile the source with the -parameters argument and Spring 4+ is being used, then the standard JDK reflection API is used to discover the parameter names. This works on both classes and interfaces.
-
Last, if the code was compiled with the debug symbols, the parameter names will be discovered using the debug symbols. This will not work for interfaces since they do not have debug information about the parameter names. For interfaces, annotations or the JDK 8 approach must be used.
Any Spring-EL functionality is available within the expression, so you can also access properties on the arguments. For example, if you wanted a particular method to only allow access to a user whose username matched that of the contact, you could write
@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
@PreAuthorize("#contact.name == authentication.name")
fun doSomething(contact: Contact?)
Here we are accessing another built-in expression, authentication
, which is the Authentication
stored in the security context.
You can also access its "principal" property directly, using the expression principal
.
The value will often be a UserDetails
instance, so you might use an expression like principal.username
or principal.enabled
.
Filtering using @PreFilter and @PostFilter
Spring Security supports filtering of collections, arrays, maps and streams using expressions. This is most commonly performed on the return value of a method. For example:
@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();
@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
fun getAll(): List<Contact?>
When using the @PostFilter
annotation, Spring Security iterates through the returned collection or map and removes any elements for which the supplied expression is false.
For an array, a new array instance will be returned containing filtered elements.
The name filterObject
refers to the current object in the collection.
In case when a map is used it will refer to the current Map.Entry
object which allows one to use filterObject.key
or filterObject.value
in the expresion.
You can also filter before the method call, using @PreFilter
, though this is a less common requirement.
The syntax is just the same, but if there is more than one argument which is a collection type then you have to select one by name using the filterTarget
property of this annotation.
Note that filtering is obviously not a substitute for tuning your data retrieval queries. If you are filtering large collections and removing many of the entries then this is likely to be inefficient.
Built-In Expressions
There are some built-in expressions which are specific to method security, which we have already seen in use above.
The filterTarget
and returnValue
values are simple enough, but the use of the hasPermission()
expression warrants a closer look.
The PermissionEvaluator interface
hasPermission()
expressions are delegated to an instance of PermissionEvaluator
.
It is intended to bridge between the expression system and Spring Security’s ACL system, allowing you to specify authorization constraints on domain objects, based on abstract permissions.
It has no explicit dependencies on the ACL module, so you could swap that out for an alternative implementation if required.
The interface has two methods:
boolean hasPermission(Authentication authentication, Object targetDomainObject,
Object permission);
boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission);
which map directly to the available versions of the expression, with the exception that the first argument (the Authentication
object) is not supplied.
The first is used in situations where the domain object, to which access is being controlled, is already loaded.
Then expression will return true if the current user has the given permission for that object.
The second version is used in cases where the object is not loaded, but its identifier is known.
An abstract "type" specifier for the domain object is also required, allowing the correct ACL permissions to be loaded.
This has traditionally been the Java class of the object, but does not have to be as long as it is consistent with how the permissions are loaded.
To use hasPermission()
expressions, you have to explicitly configure a PermissionEvaluator
in your application context.
This would look something like this:
<security:global-method-security pre-post-annotations="enabled">
<security:expression-handler ref="expressionHandler"/>
</security:global-method-security>
<bean id="expressionHandler" class=
"org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
<property name="permissionEvaluator" ref="myPermissionEvaluator"/>
</bean>
Where myPermissionEvaluator
is the bean which implements PermissionEvaluator
.
Usually this will be the implementation from the ACL module which is called AclPermissionEvaluator
.
See the "Contacts" sample application configuration for more details.
Method Security Meta Annotations
You can make use of meta annotations for method security to make your code more readable. This is especially convenient if you find that you are repeating the same complex expression throughout your code base. For example, consider the following:
@PreAuthorize("#contact.name == authentication.name")
Instead of repeating this everywhere, we can create a meta annotation that can be used instead.
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}
@Retention(AnnotationRetention.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
annotation class ContactPermission
Meta annotations can be used for any of the Spring Security method security annotations. In order to remain compliant with the specification JSR-250 annotations do not support meta annotations.
11.4. Secure Object Implementations
11.4.1. AOP Alliance (MethodInvocation) Security Interceptor
Prior to Spring Security 2.0, securing MethodInvocation
s needed quite a lot of boiler plate configuration.
Now the recommended approach for method security is to use namespace configuration.
This way the method security infrastructure beans are configured automatically for you so you don’t really need to know about the implementation classes.
We’ll just provide a quick overview of the classes that are involved here.
Method security is enforced using a MethodSecurityInterceptor
, which secures MethodInvocation
s.
Depending on the configuration approach, an interceptor may be specific to a single bean or shared between multiple beans.
The interceptor uses a MethodSecurityMetadataSource
instance to obtain the configuration attributes that apply to a particular method invocation.
MapBasedMethodSecurityMetadataSource
is used to store configuration attributes keyed by method names (which can be wildcarded) and will be used internally when the attributes are defined in the application context using the <intercept-methods>
or <protect-point>
elements.
Other implementations will be used to handle annotation-based configuration.
Explicit MethodSecurityInterceptor Configuration
You can of course configure a MethodSecurityInterceptor
directly in your application context for use with one of Spring AOP’s proxying mechanisms:
<bean id="bankManagerSecurity" class=
"org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
<sec:method-security-metadata-source>
<sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
<sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
</sec:method-security-metadata-source>
</property>
</bean>
11.4.2. AspectJ (JoinPoint) Security Interceptor
The AspectJ security interceptor is very similar to the AOP Alliance security interceptor discussed in the previous section. Indeed we will only discuss the differences in this section.
The AspectJ interceptor is named AspectJSecurityInterceptor
.
Unlike the AOP Alliance security interceptor, which relies on the Spring application context to weave in the security interceptor via proxying, the AspectJSecurityInterceptor
is weaved in via the AspectJ compiler.
It would not be uncommon to use both types of security interceptors in the same application, with AspectJSecurityInterceptor
being used for domain object instance security and the AOP Alliance MethodSecurityInterceptor
being used for services layer security.
Let’s first consider how the AspectJSecurityInterceptor
is configured in the Spring application context:
<bean id="bankManagerSecurity" class=
"org.springframework.security.access.intercept.aspectj.AspectJMethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
<sec:method-security-metadata-source>
<sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
<sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
</sec:method-security-metadata-source>
</property>
</bean>
As you can see, aside from the class name, the AspectJSecurityInterceptor
is exactly the same as the AOP Alliance security interceptor.
Indeed the two interceptors can share the same securityMetadataSource
, as the SecurityMetadataSource
works with java.lang.reflect.Method
s rather than an AOP library-specific class.
Of course, your access decisions have access to the relevant AOP library-specific invocation (ie MethodInvocation
or JoinPoint
) and as such can consider a range of addition criteria when making access decisions (such as method arguments).
Next you’ll need to define an AspectJ aspect
.
For example:
package org.springframework.security.samples.aspectj;
import org.springframework.security.access.intercept.aspectj.AspectJSecurityInterceptor;
import org.springframework.security.access.intercept.aspectj.AspectJCallback;
import org.springframework.beans.factory.InitializingBean;
public aspect DomainObjectInstanceSecurityAspect implements InitializingBean {
private AspectJSecurityInterceptor securityInterceptor;
pointcut domainObjectInstanceExecution(): target(PersistableEntity)
&& execution(public * *(..)) && !within(DomainObjectInstanceSecurityAspect);
Object around(): domainObjectInstanceExecution() {
if (this.securityInterceptor == null) {
return proceed();
}
AspectJCallback callback = new AspectJCallback() {
public Object proceedWithObject() {
return proceed();
}
};
return this.securityInterceptor.invoke(thisJoinPoint, callback);
}
public AspectJSecurityInterceptor getSecurityInterceptor() {
return securityInterceptor;
}
public void setSecurityInterceptor(AspectJSecurityInterceptor securityInterceptor) {
this.securityInterceptor = securityInterceptor;
}
public void afterPropertiesSet() throws Exception {
if (this.securityInterceptor == null)
throw new IllegalArgumentException("securityInterceptor required");
}
}
}
In the above example, the security interceptor will be applied to every instance of PersistableEntity
, which is an abstract class not shown (you can use any other class or pointcut
expression you like).
For those curious, AspectJCallback
is needed because the proceed();
statement has special meaning only within an around()
body.
The AspectJSecurityInterceptor
calls this anonymous AspectJCallback
class when it wants the target object to continue.
You will need to configure Spring to load the aspect and wire it with the AspectJSecurityInterceptor
.
A bean declaration which achieves this is shown below:
<bean id="domainObjectInstanceSecurityAspect"
class="security.samples.aspectj.DomainObjectInstanceSecurityAspect"
factory-method="aspectOf">
<property name="securityInterceptor" ref="bankManagerSecurity"/>
</bean>
That’s it!
Now you can create your beans from anywhere within your application, using whatever means you think fit (e.g. new Person();
) and they will have the security interceptor applied.
11.5. Method Security
From version 2.0 onwards Spring Security has improved support substantially for adding security to your service layer methods.
It provides support for JSR-250 annotation security as well as the framework’s original @Secured
annotation.
From 3.0 you can also make use of new expression-based annotations.
You can apply security to a single bean, using the intercept-methods
element to decorate the bean declaration, or you can secure multiple beans across the entire service layer using the AspectJ style pointcuts.
11.5.1. EnableMethodSecurity
In Spring Security 5.6, we can enable annotation-based security using the @EnableMethodSecurity
annotation on any @Configuration
instance.
This improves upon @EnableGlobalMethodSecurity
in a number of ways. @EnableMethodSecurity
:
-
Uses the simplified
AuthorizationManager
API instead of metadata sources, config attributes, decision managers, and voters. This simplifies reuse and customization. -
Favors direct bean-based configuration, instead of requiring extending
GlobalMethodSecurityConfiguration
to customize beans -
Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize
-
Checks for conflicting annotations to ensure an unambiguous security configuration
-
Complies with JSR-250
-
Enables
@PreAuthorize
,@PostAuthorize
,@PreFilter
, and@PostFilter
by default
For earlier versions, please read about similar support with @EnableGlobalMethodSecurity. |
For example, the following would enable Spring Security’s @PreAuthorize
annotation:
@EnableMethodSecurity
public class MethodSecurityConfig {
// ...
}
@EnableMethodSecurity
class MethodSecurityConfig {
// ...
}
<sec:method-security/>
Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly.
Spring Security’s native annotation support defines a set of attributes for the method.
These will be passed to the DefaultAuthorizationMethodInterceptorChain
for it to make the actual decision:
public interface BankService {
@PreAuthorize("hasRole('USER')")
Account readAccount(Long id);
@PreAuthorize("hasRole('USER')")
List<Account> findAccounts();
@PreAuthorize("hasRole('TELLER')")
Account post(Account account, Double amount);
}
interface BankService {
@PreAuthorize("hasRole('USER')")
fun readAccount(id : Long) : Account
@PreAuthorize("hasRole('USER')")
fun findAccounts() : List<Account>
@PreAuthorize("hasRole('TELLER')")
fun post(account : Account, amount : Double) : Account
}
You can enable support for Spring Security’s @Secured
annotation using:
@EnableMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}
@EnableMethodSecurity(securedEnabled = true)
class MethodSecurityConfig {
// ...
}
<sec:method-security secured-enabled="true"/>
or JSR-250 using:
@EnableMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}
@EnableMethodSecurity(jsr250Enabled = true)
class MethodSecurityConfig {
// ...
}
<sec:method-security jsr250-enabled="true"/>
Customizing Authorization
Spring Security’s @PreAuthorize
, @PostAuthorize
, @PreFilter
, and @PostFilter
ship with rich expression-based support.
If you need to customize the way that expressions are handled, you can expose a custom MethodSecurityExpressionHandler
, like so:
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setTrustResolver(myCustomTrustResolver);
return handler;
}
companion object {
@Bean
fun methodSecurityExpressionHandler() : MethodSecurityExpressionHandler {
val handler = DefaultMethodSecurityExpressionHandler();
handler.setTrustResolver(myCustomTrustResolver);
return handler;
}
}
<sec:method-security>
<sec:expression-handler ref="myExpressionHandler"/>
</sec:method-security>
<bean id="myExpressionHandler"
class="org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler">
<property name="trustResolver" ref="myCustomTrustResolver"/>
</bean>
We expose |
Also, for role-based authorization, Spring Security adds a default ROLE_
prefix, which is uses when evaluating expressions like hasRole
.
@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("MYPREFIX_");
}
companion object {
@Bean
fun grantedAuthorityDefaults() : GrantedAuthorityDefaults {
return GrantedAuthorityDefaults("MYPREFIX_");
}
}
<sec:method-security/>
<bean id="grantedAuthorityDefaults" class="org.springframework.security.config.core.GrantedAuthorityDefaults">
<constructor-arg value="MYPREFIX_"/>
</bean>
We expose |
Custom Authorization Managers
Method authorization is a combination of before- and after-method authorization.
Before-method authorization is performed before the method is invoked.
If that authorization denies access, the method is not invoked, and an |
To recreate what adding @EnableMethodSecurity
does by default, you would publish the following configuration:
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preFilterAuthorizationMethodInterceptor() {
return new PreFilterAuthorizationMethodInterceptor();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorizeAuthorizationMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorizeAuthorizationMethodInterceptor() {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postFilterAuthorizationMethodInterceptor() {
return new PostFilterAuthorizationMethodInterceptor();
}
}
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun preFilterAuthorizationMethodInterceptor() : Advisor {
return PreFilterAuthorizationMethodInterceptor();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun preAuthorizeAuthorizationMethodInterceptor() : Advisor {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postAuthorizeAuthorizationMethodInterceptor() : Advisor {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postFilterAuthorizationMethodInterceptor() : Advisor {
return PostFilterAuthorizationMethodInterceptor();
}
}
<sec:method-security pre-post-enabled="false"/>
<aop:config/>
<bean id="preFilterAuthorizationMethodInterceptor"
class="org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor"/>
<bean id="preAuthorizeAuthorizationMethodInterceptor"
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
factory-method="preAuthorize"/>
<bean id="postAuthorizeAuthorizationMethodInterceptor"
class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor"
factory-method="postAuthorize"/>
<bean id="postFilterAuthorizationMethodInterceptor"
class="org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor"/>
Notice that Spring Security’s method security is built using Spring AOP.
So, interceptors are invoked based on the order specified.
This can be customized by calling setOrder
on the interceptor instances like so:
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postFilterAuthorizationMethodInterceptor() {
PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationMethodInterceptor();
interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1);
return interceptor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postFilterAuthorizationMethodInterceptor() : Advisor {
val interceptor = PostFilterAuthorizationMethodInterceptor();
interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1);
return interceptor;
}
<bean id="postFilterAuthorizationMethodInterceptor"
class="org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor">
<property name="order"
value="#{T(org.springframework.security.authorization.method.AuthorizationInterceptorsOrder).POST_AUTHORIZE.getOrder() -1}"/>
</bean>
You may want to only support @PreAuthorize
in your application, in which case you can do the following:
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize() {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
}
}
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun preAuthorize() : Advisor {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize()
}
}
<sec:method-security pre-post-enabled="false"/>
<aop:config/>
<bean id="preAuthorizeAuthorizationMethodInterceptor"
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
factory-method="preAuthorize"/>
Or, you may have a custom before-method AuthorizationManager
that you want to add to the list.
In this case, you will need to tell Spring Security both the AuthorizationManager
and to which methods and classes your authorization manager applies.
Thus, you can configure Spring Security to invoke your AuthorizationManager
in between @PreAuthorize
and @PostAuthorize
like so:
@EnableMethodSecurity
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public Advisor customAuthorize() {
JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
pattern.setPattern("org.mycompany.myapp.service.*");
AuthorizationManager<MethodInvocation> rule = AuthorityAuthorizationManager.isAuthenticated();
AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(pattern, rule);
interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
return interceptor;
}
}
@EnableMethodSecurity
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun customAuthorize() : Advisor {
val pattern = JdkRegexpMethodPointcut();
pattern.setPattern("org.mycompany.myapp.service.*");
val rule = AuthorityAuthorizationManager.isAuthenticated();
val interceptor = AuthorizationManagerBeforeMethodInterceptor(pattern, rule);
interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
return interceptor;
}
}
<sec:method-security/>
<aop:config/>
<bean id="customAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor">
<constructor-arg>
<bean class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="pattern" value="org.mycompany.myapp.service.*"/>
</bean>
</constructor-arg>
<constructor-arg>
<bean class="org.springframework.security.authorization.AuthorityAuthorizationManager"
factory-method="isAuthenticated"/>
</constructor-arg>
<property name="order"
value="#{T(org.springframework.security.authorization.method.AuthorizationInterceptorsOrder).PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1}"/>
</bean>
You can place your interceptor in between Spring Security method interceptors using the order constants specified in |
The same can be done for after-method authorization. After-method authorization is generally concerned with analysing the return value to verify access.
For example, you might have a method that confirms that the account requested actually belongs to the logged-in user like so:
public interface BankService {
@PreAuthorize("hasRole('USER')")
@PostAuthorize("returnObject.owner == authentication.name")
Account readAccount(Long id);
}
interface BankService {
@PreAuthorize("hasRole('USER')")
@PostAuthorize("returnObject.owner == authentication.name")
fun readAccount(id : Long) : Account
}
You can supply your own AuthorizationMethodInterceptor
to customize how access to the return value is evaluated.
For example, if you have your own custom annotation, you can configure it like so:
@EnableMethodSecurity
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public Advisor customAuthorize(AuthorizationManager<MethodInvocationResult> rules) {
AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(pattern, rules);
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
return interceptor;
}
}
@EnableMethodSecurity
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun customAuthorize(rules : AuthorizationManager<MethodInvocationResult>) : Advisor {
val pattern = AnnotationMethodMatcher(MySecurityAnnotation::class.java);
val interceptor = AuthorizationManagerAfterMethodInterceptor(pattern, rules);
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
return interceptor;
}
}
<sec:method-security/>
<aop:config/>
<bean id="customAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor">
<constructor-arg>
<bean class="org.springframework.aop.support.annotation.AnnotationMethodMatcher">
<constructor-arg value="#{T(org.mycompany.MySecurityAnnotation)}"/>
</bean>
</constructor-arg>
<constructor-arg>
<bean class="org.springframework.security.authorization.AuthorityAuthorizationManager"
factory-method="isAuthenticated"/>
</constructor-arg>
<property name="order"
value="#{T(org.springframework.security.authorization.method.AuthorizationInterceptorsOrder).PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1}"/>
</bean>
and it will be invoked after the @PostAuthorize
interceptor.
11.5.2. EnableGlobalMethodSecurity
We can enable annotation-based security using the @EnableGlobalMethodSecurity
annotation on any @Configuration
instance.
For example, the following would enable Spring Security’s @Secured
annotation.
@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}
@EnableGlobalMethodSecurity(securedEnabled = true)
open class MethodSecurityConfig {
// ...
}
Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly. Spring Security’s native annotation support defines a set of attributes for the method. These will be passed to the AccessDecisionManager for it to make the actual decision:
public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
fun readAccount(id: Long): Account
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
fun findAccounts(): Array<Account>
@Secured("ROLE_TELLER")
fun post(account: Account, amount: Double): Account
}
Support for JSR-250 annotations can be enabled using
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}
@EnableGlobalMethodSecurity(jsr250Enabled = true)
open class MethodSecurityConfig {
// ...
}
These are standards-based and allow simple role-based constraints to be applied but do not have the power Spring Security’s native annotations. To use the new expression-based syntax, you would use
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}
@EnableGlobalMethodSecurity(prePostEnabled = true)
open class MethodSecurityConfig {
// ...
}
and the equivalent Java code would be
public interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}
interface BankService {
@PreAuthorize("isAnonymous()")
fun readAccount(id: Long): Account
@PreAuthorize("isAnonymous()")
fun findAccounts(): Array<Account>
@PreAuthorize("hasAuthority('ROLE_TELLER')")
fun post(account: Account, amount: Double): Account
}
11.5.3. GlobalMethodSecurityConfiguration
Sometimes you may need to perform operations that are more complicated than are possible with the @EnableGlobalMethodSecurity
annotation allow.
For these instances, you can extend the GlobalMethodSecurityConfiguration
ensuring that the @EnableGlobalMethodSecurity
annotation is present on your subclass.
For example, if you wanted to provide a custom MethodSecurityExpressionHandler
, you could use the following configuration:
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
// ... create and return custom MethodSecurityExpressionHandler ...
return expressionHandler;
}
}
@EnableGlobalMethodSecurity(prePostEnabled = true)
open class MethodSecurityConfig : GlobalMethodSecurityConfiguration() {
override fun createExpressionHandler(): MethodSecurityExpressionHandler {
// ... create and return custom MethodSecurityExpressionHandler ...
return expressionHandler
}
}
For additional information about methods that can be overridden, refer to the GlobalMethodSecurityConfiguration
Javadoc.
11.5.4. The <global-method-security> Element
This element is used to enable annotation-based security in your application (by setting the appropriate attributes on the element), and also to group together security pointcut declarations which will be applied across your entire application context.
You should only declare one <global-method-security>
element.
The following declaration would enable support for Spring Security’s @Secured
:
<global-method-security secured-annotations="enabled" />
Adding an annotation to a method (on an class or interface) would then limit the access to that method accordingly.
Spring Security’s native annotation support defines a set of attributes for the method.
These will be passed to the AccessDecisionManager
for it to make the actual decision:
public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
fun readAccount(id: Long): Account
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
fun findAccounts(): Array<Account>
@Secured("ROLE_TELLER")
fun post(account: Account, amount: Double): Account
}
Support for JSR-250 annotations can be enabled using
<global-method-security jsr250-annotations="enabled" />
These are standards-based and allow simple role-based constraints to be applied but do not have the power Spring Security’s native annotations. To use the new expression-based syntax, you would use
<global-method-security pre-post-annotations="enabled" />
and the equivalent Java code would be
public interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}
interface BankService {
@PreAuthorize("isAnonymous()")
fun readAccount(id: Long): Account
@PreAuthorize("isAnonymous()")
fun findAccounts(): Array<Account>
@PreAuthorize("hasAuthority('ROLE_TELLER')")
fun post(account: Account, amount: Double): Account
}
Expression-based annotations are a good choice if you need to define simple rules that go beyond checking the role names against the user’s list of authorities.
The annotated methods will only be secured for instances which are defined as Spring beans (in the same application context in which method-security is enabled).
If you want to secure instances which are not created by Spring (using the |
You can enable more than one type of annotation in the same application, but only one type should be used for any interface or class as the behaviour will not be well-defined otherwise. If two annotations are found which apply to a particular method, then only one of them will be applied. |
11.5.5. Adding Security Pointcuts using protect-pointcut
The use of protect-pointcut
is particularly powerful, as it allows you to apply security to many beans with only a simple declaration.
Consider the following example:
<global-method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))"
access="ROLE_USER"/>
</global-method-security>
This will protect all methods on beans declared in the application context whose classes are in the com.mycompany
package and whose class names end in "Service".
Only users with the ROLE_USER
role will be able to invoke these methods.
As with URL matching, the most specific matches must come first in the list of pointcuts, as the first matching expression will be used.
Security annotations take precedence over pointcuts.
11.6. Domain Object Security (ACLs)
11.6.1. Overview
Complex applications often will find the need to define access permissions not simply at a web request or method invocation level.
Instead, security decisions need to comprise both who (Authentication
), where (MethodInvocation
) and what (SomeDomainObject
).
In other words, authorization decisions also need to consider the actual domain object instance subject of a method invocation.
Imagine you’re designing an application for a pet clinic. There will be two main groups of users of your Spring-based application: staff of the pet clinic, as well as the pet clinic’s customers. The staff will have access to all of the data, whilst your customers will only be able to see their own customer records. To make it a little more interesting, your customers can allow other users to see their customer records, such as their "puppy preschool" mentor or president of their local "Pony Club". Using Spring Security as the foundation, you have several approaches that can be used:
-
Write your business methods to enforce the security. You could consult a collection within the
Customer
domain object instance to determine which users have access. By using theSecurityContextHolder.getContext().getAuthentication()
, you’ll be able to access theAuthentication
object. -
Write an
AccessDecisionVoter
to enforce the security from theGrantedAuthority[]
s stored in theAuthentication
object. This would mean yourAuthenticationManager
would need to populate theAuthentication
with customGrantedAuthority[]
s representing each of theCustomer
domain object instances the principal has access to. -
Write an
AccessDecisionVoter
to enforce the security and open the targetCustomer
domain object directly. This would mean your voter needs access to a DAO that allows it to retrieve theCustomer
object. It would then access theCustomer
object’s collection of approved users and make the appropriate decision.
Each one of these approaches is perfectly legitimate.
However, the first couples your authorization checking to your business code.
The main problems with this include the enhanced difficulty of unit testing and the fact it would be more difficult to reuse the Customer
authorization logic elsewhere.
Obtaining the GrantedAuthority[]
s from the Authentication
object is also fine, but will not scale to large numbers of Customer
s.
If a user might be able to access 5,000 Customer
s (unlikely in this case, but imagine if it were a popular vet for a large Pony Club!) the amount of memory consumed and time required to construct the Authentication
object would be undesirable.
The final method, opening the Customer
directly from external code, is probably the best of the three.
It achieves separation of concerns, and doesn’t misuse memory or CPU cycles, but it is still inefficient in that both the AccessDecisionVoter
and the eventual business method itself will perform a call to the DAO responsible for retrieving the Customer
object.
Two accesses per method invocation is clearly undesirable.
In addition, with every approach listed you’ll need to write your own access control list (ACL) persistence and business logic from scratch.
Fortunately, there is another alternative, which we’ll talk about below.
11.6.2. Key Concepts
Spring Security’s ACL services are shipped in the spring-security-acl-xxx.jar
.
You will need to add this JAR to your classpath to use Spring Security’s domain object instance security capabilities.
Spring Security’s domain object instance security capabilities centre on the concept of an access control list (ACL). Every domain object instance in your system has its own ACL, and the ACL records details of who can and can’t work with that domain object. With this in mind, Spring Security delivers three main ACL-related capabilities to your application:
-
A way of efficiently retrieving ACL entries for all of your domain objects (and modifying those ACLs)
-
A way of ensuring a given principal is permitted to work with your objects, before methods are called
-
A way of ensuring a given principal is permitted to work with your objects (or something they return), after methods are called
As indicated by the first bullet point, one of the main capabilities of the Spring Security ACL module is providing a high-performance way of retrieving ACLs. This ACL repository capability is extremely important, because every domain object instance in your system might have several access control entries, and each ACL might inherit from other ACLs in a tree-like structure (this is supported out-of-the-box by Spring Security, and is very commonly used). Spring Security’s ACL capability has been carefully designed to provide high performance retrieval of ACLs, together with pluggable caching, deadlock-minimizing database updates, independence from ORM frameworks (we use JDBC directly), proper encapsulation, and transparent database updating.
Given databases are central to the operation of the ACL module, let’s explore the four main tables used by default in the implementation. The tables are presented below in order of size in a typical Spring Security ACL deployment, with the table with the most rows listed last:
-
ACL_SID allows us to uniquely identify any principal or authority in the system ("SID" stands for "security identity"). The only columns are the ID, a textual representation of the SID, and a flag to indicate whether the textual representation refers to a principal name or a
GrantedAuthority
. Thus, there is a single row for each unique principal orGrantedAuthority
. When used in the context of receiving a permission, a SID is generally called a "recipient". -
ACL_CLASS allows us to uniquely identify any domain object class in the system. The only columns are the ID and the Java class name. Thus, there is a single row for each unique Class we wish to store ACL permissions for.
-
ACL_OBJECT_IDENTITY stores information for each unique domain object instance in the system. Columns include the ID, a foreign key to the ACL_CLASS table, a unique identifier so we know which ACL_CLASS instance we’re providing information for, the parent, a foreign key to the ACL_SID table to represent the owner of the domain object instance, and whether we allow ACL entries to inherit from any parent ACL. We have a single row for every domain object instance we’re storing ACL permissions for.
-
Finally, ACL_ENTRY stores the individual permissions assigned to each recipient. Columns include a foreign key to the ACL_OBJECT_IDENTITY, the recipient (i.e. a foreign key to ACL_SID), whether we’ll be auditing or not, and the integer bit mask that represents the actual permission being granted or denied. We have a single row for every recipient that receives a permission to work with a domain object.
As mentioned in the last paragraph, the ACL system uses integer bit masking.
Don’t worry, you need not be aware of the finer points of bit shifting to use the ACL system, but suffice to say that we have 32 bits we can switch on or off.
Each of these bits represents a permission, and by default the permissions are read (bit 0), write (bit 1), create (bit 2), delete (bit 3) and administer (bit 4).
It’s easy to implement your own Permission
instance if you wish to use other permissions, and the remainder of the ACL framework will operate without knowledge of your extensions.
It is important to understand that the number of domain objects in your system has absolutely no bearing on the fact we’ve chosen to use integer bit masking. Whilst you have 32 bits available for permissions, you could have billions of domain object instances (which will mean billions of rows in ACL_OBJECT_IDENTITY and quite probably ACL_ENTRY). We make this point because we’ve found sometimes people mistakenly believe they need a bit for each potential domain object, which is not the case.
Now that we’ve provided a basic overview of what the ACL system does, and what it looks like at a table structure, let’s explore the key interfaces. The key interfaces are:
-
Acl
: Every domain object has one and only oneAcl
object, which internally holds theAccessControlEntry
s as well as knows the owner of theAcl
. An Acl does not refer directly to the domain object, but instead to anObjectIdentity
. TheAcl
is stored in the ACL_OBJECT_IDENTITY table. -
AccessControlEntry
: AnAcl
holds multipleAccessControlEntry
s, which are often abbreviated as ACEs in the framework. Each ACE refers to a specific tuple ofPermission
,Sid
andAcl
. An ACE can also be granting or non-granting and contain audit settings. The ACE is stored in the ACL_ENTRY table. -
Permission
: A permission represents a particular immutable bit mask, and offers convenience functions for bit masking and outputting information. The basic permissions presented above (bits 0 through 4) are contained in theBasePermission
class. -
Sid
: The ACL module needs to refer to principals andGrantedAuthority[]
s. A level of indirection is provided by theSid
interface, which is an abbreviation of "security identity". Common classes includePrincipalSid
(to represent the principal inside anAuthentication
object) andGrantedAuthoritySid
. The security identity information is stored in the ACL_SID table. -
ObjectIdentity
: Each domain object is represented internally within the ACL module by anObjectIdentity
. The default implementation is calledObjectIdentityImpl
. -
AclService
: Retrieves theAcl
applicable for a givenObjectIdentity
. In the included implementation (JdbcAclService
), retrieval operations are delegated to aLookupStrategy
. TheLookupStrategy
provides a highly optimized strategy for retrieving ACL information, using batched retrievals (BasicLookupStrategy
) and supporting custom implementations that leverage materialized views, hierarchical queries and similar performance-centric, non-ANSI SQL capabilities. -
MutableAclService
: Allows a modifiedAcl
to be presented for persistence. It is not essential to use this interface if you do not wish.
Please note that our out-of-the-box AclService and related database classes all use ANSI SQL. This should therefore work with all major databases. At the time of writing, the system had been successfully tested using Hypersonic SQL, PostgreSQL, Microsoft SQL Server and Oracle.
Two samples ship with Spring Security that demonstrate the ACL module. The first is the Contacts Sample, and the other is the Document Management System (DMS) Sample. We suggest taking a look over these for examples.
11.6.3. Getting Started
To get starting using Spring Security’s ACL capability, you will need to store your ACL information somewhere.
This necessitates the instantiation of a DataSource
using Spring.
The DataSource
is then injected into a JdbcMutableAclService
and BasicLookupStrategy
instance.
The latter provides high-performance ACL retrieval capabilities, and the former provides mutator capabilities.
Refer to one of the samples that ship with Spring Security for an example configuration.
You’ll also need to populate the database with the four ACL-specific tables listed in the last section (refer to the ACL samples for the appropriate SQL statements).
Once you’ve created the required schema and instantiated JdbcMutableAclService
, you’ll next need to ensure your domain model supports interoperability with the Spring Security ACL package.
Hopefully ObjectIdentityImpl
will prove sufficient, as it provides a large number of ways in which it can be used.
Most people will have domain objects that contain a public Serializable getId()
method.
If the return type is long, or compatible with long (e.g. an int), you will find you need not give further consideration to ObjectIdentity
issues.
Many parts of the ACL module rely on long identifiers.
If you’re not using long (or an int, byte etc), there is a very good chance you’ll need to reimplement a number of classes.
We do not intend to support non-long identifiers in Spring Security’s ACL module, as longs are already compatible with all database sequences, the most common identifier data type, and are of sufficient length to accommodate all common usage scenarios.
The following fragment of code shows how to create an Acl
, or modify an existing Acl
:
// Prepare the information we'd like in our access control entry (ACE)
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;
// Create or update the relevant ACL
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}
// Now grant some permissions via an access control entry (ACE)
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);
val oi: ObjectIdentity = ObjectIdentityImpl(Foo::class.java, 44)
val sid: Sid = PrincipalSid("Samantha")
val p: Permission = BasePermission.ADMINISTRATION
// Create or update the relevant ACL
var acl: MutableAcl? = null
acl = try {
aclService.readAclById(oi) as MutableAcl
} catch (nfe: NotFoundException) {
aclService.createAcl(oi)
}
// Now grant some permissions via an access control entry (ACE)
acl!!.insertAce(acl.entries.size, p, sid, true)
aclService.updateAcl(acl)
In the example above, we’re retrieving the ACL associated with the "Foo" domain object with identifier number 44. We’re then adding an ACE so that a principal named "Samantha" can "administer" the object. The code fragment is relatively self-explanatory, except the insertAce method. The first argument to the insertAce method is determining at what position in the Acl the new entry will be inserted. In the example above, we’re just putting the new ACE at the end of the existing ACEs. The final argument is a Boolean indicating whether the ACE is granting or denying. Most of the time it will be granting (true), but if it is denying (false), the permissions are effectively being blocked.
Spring Security does not provide any special integration to automatically create, update or delete ACLs as part of your DAO or repository operations. Instead, you will need to write code like shown above for your individual domain objects. It’s worth considering using AOP on your services layer to automatically integrate the ACL information with your services layer operations. We’ve found this quite an effective approach in the past.
Once you’ve used the above techniques to store some ACL information in the database, the next step is to actually use the ACL information as part of authorization decision logic.
You have a number of choices here.
You could write your own AccessDecisionVoter
or AfterInvocationProvider
that respectively fires before or after a method invocation.
Such classes would use AclService
to retrieve the relevant ACL and then call Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode)
to decide whether permission is granted or denied.
Alternately, you could use our AclEntryVoter
, AclEntryAfterInvocationProvider
or AclEntryAfterInvocationCollectionFilteringProvider
classes.
All of these classes provide a declarative-based approach to evaluating ACL information at runtime, freeing you from needing to write any code.
Please refer to the sample applications to learn how to use these classes.
12. OAuth2
12.1. OAuth 2.0 Login
The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. GitHub) or OpenID Connect 1.0 Provider (such as Google). OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub".
OAuth 2.0 Login is implemented by using the Authorization Code Grant, as specified in the OAuth 2.0 Authorization Framework and OpenID Connect Core 1.0. |
12.1.1. Spring Boot 2.x Sample
Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login.
This section shows how to configure the OAuth 2.0 Login sample using Google as the Authentication Provider and covers the following topics:
Initial setup
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.
Google’s OAuth 2.0 implementation for authentication conforms to the OpenID Connect 1.0 specification and is OpenID Certified. |
Follow the instructions on the OpenID Connect page, starting in the section, "Setting up OAuth 2.0".
After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret.
Setting the redirect URI
The redirect URI is the path in the 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 in the previous step) on the Consent page.
In the "Set a redirect URI" sub-section, ensure that the Authorized redirect URIs field is set to http://localhost:8080/login/oauth2/code/google
.
The default redirect URI template is {baseUrl}/login/oauth2/code/{registrationId} .
The registrationId is a unique identifier for the ClientRegistration.
|
If the OAuth Client is running behind a proxy server, it is recommended to check Proxy Server Configuration to ensure the application is correctly configured.
Also, see the supported URI template variables for redirect-uri .
|
Configure application.yml
Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the authentication flow. To do so:
-
Go to
application.yml
and set the following configuration:spring: security: oauth2: client: registration: (1) google: (2) client-id: google-client-id client-secret: google-client-secret
Example 104. OAuth Client properties1 spring.security.oauth2.client.registration
is the base property prefix for OAuth Client properties.2 Following the base property prefix is the ID for the ClientRegistration, such as google. -
Replace the values in the
client-id
andclient-secret
property with the OAuth 2.0 credentials you created earlier.
Boot up the application
Launch the Spring Boot 2.x sample and go to http://localhost:8080
.
You are then redirected to the default auto-generated login page, which displays a link for Google.
Click on the Google link, and you are then redirected to Google for authentication.
After authenticating with your Google account credentials, the next page presented to you is the Consent screen. The Consent screen asks you to either allow or deny access to the OAuth Client you created earlier. Click Allow to authorize the OAuth Client to access your email address and basic profile information.
At this point, the OAuth Client retrieves your email address and basic profile information from the UserInfo Endpoint and establishes an authenticated session.
12.1.2. Spring Boot 2.x Property Mappings
The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the ClientRegistration properties.
Spring Boot 2.x | ClientRegistration |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A ClientRegistration can be initially configured using discovery of an OpenID Connect Provider’s Configuration endpoint or an Authorization Server’s Metadata endpoint, by specifying the spring.security.oauth2.client.provider.[providerId].issuer-uri property.
|
12.1.3. CommonOAuth2Provider
CommonOAuth2Provider
pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta.
For example, the authorization-uri
, token-uri
, and user-info-uri
do not change often for a Provider.
Therefore, it makes sense to provide default values in order to reduce the required configuration.
As demonstrated previously, when we configured a Google client, only the client-id
and client-secret
properties are required.
The following listing shows an example:
spring:
security:
oauth2:
client:
registration:
google:
client-id: google-client-id
client-secret: google-client-secret
The auto-defaulting of client properties works seamlessly here because the registrationId (google ) matches the GOOGLE enum (case-insensitive) in CommonOAuth2Provider .
|
For cases where you may want to specify a different registrationId
, such as google-login
, you can still leverage auto-defaulting of client properties by configuring the provider
property.
The following listing shows an example:
spring:
security:
oauth2:
client:
registration:
google-login: (1)
provider: google (2)
client-id: google-client-id
client-secret: google-client-secret
1 | The registrationId is set to google-login . |
2 | The provider property is set to google , which will leverage the auto-defaulting of client properties set in CommonOAuth2Provider.GOOGLE.getBuilder() . |
12.1.4. Configuring Custom Provider Properties
There are some OAuth 2.0 Providers that support multi-tenancy, which results in different protocol endpoints for each tenant (or sub-domain).
For example, an OAuth Client registered with Okta is assigned to a specific sub-domain and have their own protocol endpoints.
For these cases, Spring Boot 2.x provides the following base property for configuring custom provider properties: spring.security.oauth2.client.provider.[providerId]
.
The following listing shows an example:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
provider:
okta: (1)
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
user-name-attribute: sub
jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys
1 | The base property (spring.security.oauth2.client.provider.okta ) allows for custom configuration of protocol endpoint locations. |
12.1.5. Overriding Spring Boot 2.x Auto-configuration
The Spring Boot 2.x auto-configuration class for OAuth Client support is OAuth2ClientAutoConfiguration
.
It performs the following tasks:
-
Registers a
ClientRegistrationRepository
@Bean
composed ofClientRegistration
(s) from the configured OAuth Client properties. -
Provides a
WebSecurityConfigurerAdapter
@Configuration
and enables OAuth 2.0 Login throughhttpSecurity.oauth2Login()
.
If you need to override the auto-configuration based on your specific requirements, you may do so in the following ways:
Register a ClientRegistrationRepository @Bean
The following example shows how to register a ClientRegistrationRepository
@Bean
:
@Configuration
public class OAuth2LoginConfig {
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
}
private ClientRegistration googleClientRegistration() {
return ClientRegistration.withRegistrationId("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientName("Google")
.build();
}
}
@Configuration
class OAuth2LoginConfig {
@Bean
fun clientRegistrationRepository(): ClientRegistrationRepository {
return InMemoryClientRegistrationRepository(googleClientRegistration())
}
private fun googleClientRegistration(): ClientRegistration {
return ClientRegistration.withRegistrationId("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientName("Google")
.build()
}
}
Provide a WebSecurityConfigurerAdapter
The following example shows how to provide a WebSecurityConfigurerAdapter
with @EnableWebSecurity
and enable OAuth 2.0 login through httpSecurity.oauth2Login()
:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
}
}
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2Login { }
}
}
}
Completely Override the Auto-configuration
The following example shows how to completely override the auto-configuration by registering a ClientRegistrationRepository
@Bean
and providing a WebSecurityConfigurerAdapter
.
@Configuration
public class OAuth2LoginConfig {
@EnableWebSecurity
public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
}
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
}
private ClientRegistration googleClientRegistration() {
return ClientRegistration.withRegistrationId("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientName("Google")
.build();
}
}
@Configuration
class OAuth2LoginConfig {
@EnableWebSecurity
class OAuth2LoginSecurityConfig: WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2Login { }
}
}
}
@Bean
fun clientRegistrationRepository(): ClientRegistrationRepository {
return InMemoryClientRegistrationRepository(googleClientRegistration())
}
private fun googleClientRegistration(): ClientRegistration {
return ClientRegistration.withRegistrationId("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientName("Google")
.build()
}
}
12.1.6. Java Configuration without Spring Boot 2.x
If you are not able to use Spring Boot 2.x and would like to configure one of the pre-defined providers in CommonOAuth2Provider
(for example, Google), apply the following configuration:
@Configuration
public class OAuth2LoginConfig {
@EnableWebSecurity
public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
}
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
}
@Bean
public OAuth2AuthorizedClientService authorizedClientService(
ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
@Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository(
OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
private ClientRegistration googleClientRegistration() {
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.build();
}
}
@Configuration
open class OAuth2LoginConfig {
@EnableWebSecurity
open class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2Login { }
}
}
}
@Bean
open fun clientRegistrationRepository(): ClientRegistrationRepository {
return InMemoryClientRegistrationRepository(googleClientRegistration())
}
@Bean
open fun authorizedClientService(
clientRegistrationRepository: ClientRegistrationRepository?
): OAuth2AuthorizedClientService {
return InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository)
}
@Bean
open fun authorizedClientRepository(
authorizedClientService: OAuth2AuthorizedClientService?
): OAuth2AuthorizedClientRepository {
return AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService)
}
private fun googleClientRegistration(): ClientRegistration {
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.build()
}
}
<http auto-config="true">
<intercept-url pattern="/**" access="authenticated"/>
<oauth2-login authorized-client-repository-ref="authorizedClientRepository"/>
</http>
<client-registrations>
<client-registration registration-id="google"
client-id="google-client-id"
client-secret="google-client-secret"
provider-id="google"/>
</client-registrations>
<b:bean id="authorizedClientService"
class="org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService"
autowire="constructor"/>
<b:bean id="authorizedClientRepository"
class="org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository">
<b:constructor-arg ref="authorizedClientService"/>
</b:bean>
12.1.7. Advanced Configuration
HttpSecurity.oauth2Login()
provides a number of configuration options for customizing OAuth 2.0 Login.
The main configuration options are grouped into their protocol endpoint counterparts.
For example, oauth2Login().authorizationEndpoint()
allows configuring the Authorization Endpoint, whereas oauth2Login().tokenEndpoint()
allows configuring the Token Endpoint.
The following code shows an example:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
...
)
.redirectionEndpoint(redirection -> redirection
...
)
.tokenEndpoint(token -> token
...
)
.userInfoEndpoint(userInfo -> userInfo
...
)
);
}
}
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Login {
authorizationEndpoint {
...
}
redirectionEndpoint {
...
}
tokenEndpoint {
...
}
userInfoEndpoint {
...
}
}
}
}
}
The main goal of the oauth2Login()
DSL was to closely align with the naming, as defined in the specifications.
The OAuth 2.0 Authorization Framework defines the Protocol Endpoints as follows:
The authorization process utilizes two authorization server endpoints (HTTP resources):
-
Authorization Endpoint: Used by the client to obtain authorization from the resource owner via user-agent redirection.
-
Token Endpoint: Used by the client to exchange an authorization grant for an access token, typically with client authentication.
As well as one client endpoint:
-
Redirection Endpoint: Used by the authorization server to return responses containing authorization credentials to the client via the resource owner user-agent.
The OpenID Connect Core 1.0 specification defines the UserInfo Endpoint as follows:
The UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns claims about the authenticated end-user. To obtain the requested claims about the end-user, the client makes a request to the UserInfo Endpoint by using an access token obtained through OpenID Connect Authentication. These claims are normally represented by a JSON object that contains a collection of name-value pairs for the claims.
The following code shows the complete configuration options available for the oauth2Login()
DSL:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.clientRegistrationRepository(this.clientRegistrationRepository())
.authorizedClientRepository(this.authorizedClientRepository())
.authorizedClientService(this.authorizedClientService())
.loginPage("/login")
.authorizationEndpoint(authorization -> authorization
.baseUri(this.authorizationRequestBaseUri())
.authorizationRequestRepository(this.authorizationRequestRepository())
.authorizationRequestResolver(this.authorizationRequestResolver())
)
.redirectionEndpoint(redirection -> redirection
.baseUri(this.authorizationResponseBaseUri())
)
.tokenEndpoint(token -> token
.accessTokenResponseClient(this.accessTokenResponseClient())
)
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(this.userAuthoritiesMapper())
.userService(this.oauth2UserService())
.oidcUserService(this.oidcUserService())
)
);
}
}
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Login {
clientRegistrationRepository = clientRegistrationRepository()
authorizedClientRepository = authorizedClientRepository()
authorizedClientService = authorizedClientService()
loginPage = "/login"
authorizationEndpoint {
baseUri = authorizationRequestBaseUri()
authorizationRequestRepository = authorizationRequestRepository()
authorizationRequestResolver = authorizationRequestResolver()
}
redirectionEndpoint {
baseUri = authorizationResponseBaseUri()
}
tokenEndpoint {
accessTokenResponseClient = accessTokenResponseClient()
}
userInfoEndpoint {
userAuthoritiesMapper = userAuthoritiesMapper()
userService = oauth2UserService()
oidcUserService = oidcUserService()
}
}
}
}
}
In addition to the oauth2Login()
DSL, XML configuration is also supported.
The following code shows the complete configuration options available in the security namespace:
<http>
<oauth2-login client-registration-repository-ref="clientRegistrationRepository"
authorized-client-repository-ref="authorizedClientRepository"
authorized-client-service-ref="authorizedClientService"
authorization-request-repository-ref="authorizationRequestRepository"
authorization-request-resolver-ref="authorizationRequestResolver"
access-token-response-client-ref="accessTokenResponseClient"
user-authorities-mapper-ref="userAuthoritiesMapper"
user-service-ref="oauth2UserService"
oidc-user-service-ref="oidcUserService"
login-processing-url="/login/oauth2/code/*"
login-page="/login"
authentication-success-handler-ref="authenticationSuccessHandler"
authentication-failure-handler-ref="authenticationFailureHandler"
jwt-decoder-factory-ref="jwtDecoderFactory"/>
</http>
The following sections go into more detail on each of the configuration options available:
OAuth 2.0 Login Page
By default, the OAuth 2.0 Login Page is auto-generated by the DefaultLoginPageGeneratingFilter
.
The default login page shows each configured OAuth Client with its ClientRegistration.clientName
as a link, which is capable of initiating the Authorization Request (or OAuth 2.0 Login).
In order for DefaultLoginPageGeneratingFilter to show links for configured OAuth Clients, the registered ClientRegistrationRepository needs to also implement Iterable<ClientRegistration> .
See InMemoryClientRegistrationRepository for reference.
|
The link’s destination for each OAuth Client defaults to the following:
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}"
The following line shows an example:
<a href="/oauth2/authorization/google">Google</a>
To override the default login page, configure oauth2Login().loginPage()
and (optionally) oauth2Login().authorizationEndpoint().baseUri()
.
The following listing shows an example:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.loginPage("/login/oauth2")
...
.authorizationEndpoint(authorization -> authorization
.baseUri("/login/oauth2/authorization")
...
)
);
}
}
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Login {
loginPage = "/login/oauth2"
authorizationEndpoint {
baseUri = "/login/oauth2/authorization"
}
}
}
}
}
<http>
<oauth2-login login-page="/login/oauth2"
...
/>
</http>
You need to provide a @Controller with a @RequestMapping("/login/oauth2") that is capable of rendering the custom login page.
|
As noted earlier, configuring The following line shows an example:
|
Redirection Endpoint
The Redirection Endpoint is used by the Authorization Server for returning the Authorization Response (which contains the authorization credentials) to the client via the Resource Owner user-agent.
OAuth 2.0 Login leverages the Authorization Code Grant. Therefore, the authorization credential is the authorization code. |
The default Authorization Response baseUri
(redirection endpoint) is /login/oauth2/code/*
, which is defined in OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI
.
If you would like to customize the Authorization Response baseUri
, configure it as shown in the following example:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.redirectionEndpoint(redirection -> redirection
.baseUri("/login/oauth2/callback/*")
...
)
);
}
}
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Login {
redirectionEndpoint {
baseUri = "/login/oauth2/callback/*"
}
}
}
}
}
<http>
<oauth2-login login-processing-url="/login/oauth2/callback/*"
...
/>
</http>
You also need to ensure the The following listing shows an example: Java
Kotlin
|
UserInfo Endpoint
The UserInfo Endpoint includes a number of configuration options, as described in the following sub-sections:
Mapping User Authorities
After the user successfully authenticates with the OAuth 2.0 Provider, the OAuth2User.getAuthorities()
(or OidcUser.getAuthorities()
) may be mapped to a new set of GrantedAuthority
instances, which will be supplied to OAuth2AuthenticationToken
when completing the authentication.
OAuth2AuthenticationToken.getAuthorities() is used for authorizing requests, such as in hasRole('USER') or hasRole('ADMIN') .
|
There are a couple of options to choose from when mapping user authorities:
Provide an implementation of GrantedAuthoritiesMapper
and configure it as shown in the following example:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(this.userAuthoritiesMapper())
...
)
);
}
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (OidcUserAuthority.class.isInstance(authority)) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
OidcIdToken idToken = oidcUserAuthority.getIdToken();
OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
// Map the claims found in idToken and/or userInfo
// to one or more GrantedAuthority's and add it to mappedAuthorities
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
// Map the attributes found in userAttributes
// to one or more GrantedAuthority's and add it to mappedAuthorities
}
});
return mappedAuthorities;
};
}
}
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Login {
userInfoEndpoint {
userAuthoritiesMapper = userAuthoritiesMapper()
}
}
}
}
private fun userAuthoritiesMapper(): GrantedAuthoritiesMapper = GrantedAuthoritiesMapper { authorities: Collection<GrantedAuthority> ->
val mappedAuthorities = emptySet<GrantedAuthority>()
authorities.forEach { authority ->
if (authority is OidcUserAuthority) {
val idToken = authority.idToken
val userInfo = authority.userInfo
// Map the claims found in idToken and/or userInfo
// to one or more GrantedAuthority's and add it to mappedAuthorities
} else if (authority is OAuth2UserAuthority) {
val userAttributes = authority.attributes
// Map the attributes found in userAttributes
// to one or more GrantedAuthority's and add it to mappedAuthorities
}
}
mappedAuthorities
}
}
<http>
<oauth2-login user-authorities-mapper-ref="userAuthoritiesMapper"
...
/>
</http>
Alternatively, you may register a GrantedAuthoritiesMapper
@Bean
to have it automatically applied to the configuration, as shown in the following example:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(withDefaults());
}
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
...
}
}
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Login { }
}
}
@Bean
fun userAuthoritiesMapper(): GrantedAuthoritiesMapper {
...
}
}
This strategy is advanced compared to using a GrantedAuthoritiesMapper
, however, it’s also more flexible as it gives you access to the OAuth2UserRequest
and OAuth2User
(when using an OAuth 2.0 UserService) or OidcUserRequest
and OidcUser
(when using an OpenID Connect 1.0 UserService).
The OAuth2UserRequest
(and OidcUserRequest
) provides you access to the associated OAuth2AccessToken
, which is very useful in the cases where the delegator needs to fetch authority information from a protected resource before it can map the custom authorities for the user.
The following example shows how to implement and configure a delegation-based strategy using an OpenID Connect 1.0 UserService:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(this.oidcUserService())
...
)
);
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
// TODO
// 1) Fetch the authority information from the protected resource using accessToken
// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
// 3) Create a copy of oidcUser but use the mappedAuthorities instead
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return oidcUser;
};
}
}
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Login {
userInfoEndpoint {
oidcUserService = oidcUserService()
}
}
}
}
@Bean
fun oidcUserService(): OAuth2UserService<OidcUserRequest, OidcUser> {
val delegate = OidcUserService()
return OAuth2UserService { userRequest ->
// Delegate to the default implementation for loading a user
var oidcUser = delegate.loadUser(userRequest)
val accessToken = userRequest.accessToken
val mappedAuthorities = HashSet<GrantedAuthority>()
// TODO
// 1) Fetch the authority information from the protected resource using accessToken
// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
// 3) Create a copy of oidcUser but use the mappedAuthorities instead
oidcUser = DefaultOidcUser(mappedAuthorities, oidcUser.idToken, oidcUser.userInfo)
oidcUser
}
}
}
<http>
<oauth2-login oidc-user-service-ref="oidcUserService"
...
/>
</http>
OAuth 2.0 UserService
DefaultOAuth2UserService
is an implementation of an OAuth2UserService
that supports standard OAuth 2.0 Provider’s.
OAuth2UserService obtains the user attributes of the end-user (the resource owner) from the UserInfo Endpoint (by using the access token granted to the client during the authorization flow) and returns an AuthenticatedPrincipal in the form of an OAuth2User .
|
DefaultOAuth2UserService
uses a RestOperations
when requesting the user attributes at the UserInfo Endpoint.
If you need to customize the pre-processing of the UserInfo Request, you can provide DefaultOAuth2UserService.setRequestEntityConverter()
with a custom Converter<OAuth2UserRequest, RequestEntity<?>>
.
The default implementation OAuth2UserRequestEntityConverter
builds a RequestEntity
representation of a UserInfo Request that sets the OAuth2AccessToken
in the Authorization
header by default.
On the other end, if you need to customize the post-handling of the UserInfo Response, you will need to provide DefaultOAuth2UserService.setRestOperations()
with a custom configured RestOperations
.
The default RestOperations
is configured as follows:
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
OAuth2ErrorResponseErrorHandler
is a ResponseErrorHandler
that can handle an OAuth 2.0 Error (400 Bad Request).
It uses an OAuth2ErrorHttpMessageConverter
for converting the OAuth 2.0 Error parameters to an OAuth2Error
.
Whether you customize DefaultOAuth2UserService
or provide your own implementation of OAuth2UserService
, you’ll need to configure it as shown in the following example:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(this.oauth2UserService())
...
)
);
}
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
...
}
}
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Login {
userInfoEndpoint {
userService = oauth2UserService()
// ...
}
}
}
}
private fun oauth2UserService(): OAuth2UserService<OAuth2UserRequest, OAuth2User> {
// ...
}
}
OpenID Connect 1.0 UserService
OidcUserService
is an implementation of an OAuth2UserService
that supports OpenID Connect 1.0 Provider’s.
The OidcUserService
leverages the DefaultOAuth2UserService
when requesting the user attributes at the UserInfo Endpoint.
If you need to customize the pre-processing of the UserInfo Request and/or the post-handling of the UserInfo Response, you will need to provide OidcUserService.setOauth2UserService()
with a custom configured DefaultOAuth2UserService
.
Whether you customize OidcUserService
or provide your own implementation of OAuth2UserService
for OpenID Connect 1.0 Provider’s, you’ll need to configure it as shown in the following example:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(this.oidcUserService())
...
)
);
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
...
}
}
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Login {
userInfoEndpoint {
oidcUserService = oidcUserService()
// ...
}
}
}
}
private fun oidcUserService(): OAuth2UserService<OidcUserRequest, OidcUser> {
// ...
}
}
ID Token Signature Verification
OpenID Connect 1.0 Authentication introduces the ID Token, which is a security token that contains Claims about the Authentication of an End-User by an Authorization Server when used by a Client.
The ID Token is represented as a JSON Web Token (JWT) and MUST be signed using JSON Web Signature (JWS).
The OidcIdTokenDecoderFactory
provides a JwtDecoder
used for OidcIdToken
signature verification. The default algorithm is RS256
but may be different when assigned during client registration.
For these cases, a resolver may be configured to return the expected JWS algorithm assigned for a specific client.
The JWS algorithm resolver is a Function
that accepts a ClientRegistration
and returns the expected JwsAlgorithm
for the client, eg. SignatureAlgorithm.RS256
or MacAlgorithm.HS256
The following code shows how to configure the OidcIdTokenDecoderFactory
@Bean
to default to MacAlgorithm.HS256
for all ClientRegistration
:
@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
return idTokenDecoderFactory;
}
@Bean
fun idTokenDecoderFactory(): JwtDecoderFactory<ClientRegistration?> {
val idTokenDecoderFactory = OidcIdTokenDecoderFactory()
idTokenDecoderFactory.setJwsAlgorithmResolver { MacAlgorithm.HS256 }
return idTokenDecoderFactory
}
For MAC based algorithms such as HS256 , HS384 or HS512 , the client-secret corresponding to the client-id is used as the symmetric key for signature verification.
|
If more than one ClientRegistration is configured for OpenID Connect 1.0 Authentication, the JWS algorithm resolver may evaluate the provided ClientRegistration to determine which algorithm to return.
|
OpenID Connect 1.0 Logout
OpenID Connect Session Management 1.0 allows the ability to log out the End-User at the Provider using the Client. One of the strategies available is RP-Initiated Logout.
If the OpenID Provider supports both Session Management and Discovery, the client may obtain the end_session_endpoint
URL
from the OpenID Provider’s Discovery Metadata.
This can be achieved by configuring the ClientRegistration
with the issuer-uri
, as in the following example:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
...
provider:
okta:
issuer-uri: https://dev-1234.oktapreview.com
…and the OidcClientInitiatedLogoutSuccessHandler
, which implements RP-Initiated Logout, may be configured as follows:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults())
.logout(logout -> logout
.logoutSuccessHandler(oidcLogoutSuccessHandler())
);
}
private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
// Sets the location that the End-User's User Agent will be redirected to
// after the logout has been performed at the Provider
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
return oidcLogoutSuccessHandler;
}
}
NOTE: `OidcClientInitiatedLogoutSuccessHandler` supports the `{baseUrl}` placeholder.
If used, the application's base URL, like `https://app.example.org`, will replace it at request time.
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
@Autowired
private lateinit var clientRegistrationRepository: ClientRegistrationRepository
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2Login { }
logout {
logoutSuccessHandler = oidcLogoutSuccessHandler()
}
}
}
private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler {
val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository)
// Sets the location that the End-User's User Agent will be redirected to
// after the logout has been performed at the Provider
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}")
return oidcLogoutSuccessHandler
}
}
NOTE: `OidcClientInitiatedLogoutSuccessHandler` supports the `{baseUrl}` placeholder.
If used, the application's base URL, like `https://app.example.org`, will replace it at request time.
12.2. OAuth 2.0 Client
The OAuth 2.0 Client features provide support for the Client role as defined in the OAuth 2.0 Authorization Framework.
At a high-level, the core features available are:
-
WebClient
integration for Servlet Environments (for requesting protected resources)
The HttpSecurity.oauth2Client()
DSL provides a number of configuration options for customizing the core components used by OAuth 2.0 Client.
In addition, HttpSecurity.oauth2Client().authorizationCodeGrant()
enables the customization of the Authorization Code grant.
The following code shows the complete configuration options provided by the HttpSecurity.oauth2Client()
DSL:
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client(oauth2 -> oauth2
.clientRegistrationRepository(this.clientRegistrationRepository())
.authorizedClientRepository(this.authorizedClientRepository())
.authorizedClientService(this.authorizedClientService())
.authorizationCodeGrant(codeGrant -> codeGrant
.authorizationRequestRepository(this.authorizationRequestRepository())
.authorizationRequestResolver(this.authorizationRequestResolver())
.accessTokenResponseClient(this.accessTokenResponseClient())
)
);
}
}
@EnableWebSecurity
class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Client {
clientRegistrationRepository = clientRegistrationRepository()
authorizedClientRepository = authorizedClientRepository()
authorizedClientService = authorizedClientService()
authorizationCodeGrant {
authorizationRequestRepository = authorizationRequestRepository()
authorizationRequestResolver = authorizationRequestResolver()
accessTokenResponseClient = accessTokenResponseClient()
}
}
}
}
}
In addition to the HttpSecurity.oauth2Client()
DSL, XML configuration is also supported.
The following code shows the complete configuration options available in the security namespace:
<http>
<oauth2-client client-registration-repository-ref="clientRegistrationRepository"
authorized-client-repository-ref="authorizedClientRepository"
authorized-client-service-ref="authorizedClientService">
<authorization-code-grant
authorization-request-repository-ref="authorizationRequestRepository"
authorization-request-resolver-ref="authorizationRequestResolver"
access-token-response-client-ref="accessTokenResponseClient"/>
</oauth2-client>
</http>
The OAuth2AuthorizedClientManager
is responsible for managing the authorization (or re-authorization) of an OAuth 2.0 Client, in collaboration with one or more OAuth2AuthorizedClientProvider
(s).
The following code shows an example of how to register an OAuth2AuthorizedClientManager
@Bean
and associate it with an OAuth2AuthorizedClientProvider
composite that provides support for the authorization_code
, refresh_token
, client_credentials
and password
authorization grant types:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ClientRegistrationRepository,
authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager {
val authorizedClientProvider: OAuth2AuthorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
The following sections will go into more detail on the core components used by OAuth 2.0 Client and the configuration options available:
12.2.1. Core Interfaces / Classes
ClientRegistration
ClientRegistration
is a representation of a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider.
A client registration holds information, such as client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI, and other details.
ClientRegistration
and its properties are defined as follows:
public final class ClientRegistration {
private String registrationId; (1)
private String clientId; (2)
private String clientSecret; (3)
private ClientAuthenticationMethod clientAuthenticationMethod; (4)
private AuthorizationGrantType authorizationGrantType; (5)
private String redirectUri; (6)
private Set<String> scopes; (7)
private ProviderDetails providerDetails;
private String clientName; (8)
public class ProviderDetails {
private String authorizationUri; (9)
private String tokenUri; (10)
private UserInfoEndpoint userInfoEndpoint;
private String jwkSetUri; (11)
private String issuerUri; (12)
private Map<String, Object> configurationMetadata; (13)
public class UserInfoEndpoint {
private String uri; (14)
private AuthenticationMethod authenticationMethod; (15)
private String userNameAttributeName; (16)
}
}
}
1 | registrationId : The ID that uniquely identifies the ClientRegistration . |
2 | clientId : The client identifier. |
3 | clientSecret : The client secret. |
4 | clientAuthenticationMethod : The method used to authenticate the Client with the Provider.
The supported values are client_secret_basic, client_secret_post, private_key_jwt, client_secret_jwt and none (public clients). |
5 | authorizationGrantType : The OAuth 2.0 Authorization Framework defines four Authorization Grant types.
The supported values are authorization_code , client_credentials , password , as well as, extension grant type urn:ietf:params:oauth:grant-type:jwt-bearer . |
6 | redirectUri : 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 to the client. |
7 | scopes : The scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. |
8 | clientName : A descriptive name used for the client.
The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. |
9 | authorizationUri : The Authorization Endpoint URI for the Authorization Server. |
10 | tokenUri : The Token Endpoint URI for the Authorization Server. |
11 | jwkSetUri : The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server,
which contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID Token and optionally the UserInfo Response. |
12 | issuerUri : Returns the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server. |
13 | configurationMetadata : The OpenID Provider Configuration Information.
This information will only be available if the Spring Boot 2.x property spring.security.oauth2.client.provider.[providerId].issuerUri is configured. |
14 | (userInfoEndpoint)uri : The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. |
15 | (userInfoEndpoint)authenticationMethod : The authentication method used when sending the access token to the UserInfo Endpoint.
The supported values are header, form and query. |
16 | userNameAttributeName : The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. |
A ClientRegistration
can be initially configured using discovery of an OpenID Connect Provider’s Configuration endpoint or an Authorization Server’s Metadata endpoint.
ClientRegistrations
provides convenience methods for configuring a ClientRegistration
in this way, as can be seen in the following example:
ClientRegistration clientRegistration =
ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build();
val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build()
The above code will query in series https://idp.example.com/issuer/.well-known/openid-configuration
, and then https://idp.example.com/.well-known/openid-configuration/issuer
, and finally https://idp.example.com/.well-known/oauth-authorization-server/issuer
, stopping at the first to return a 200 response.
As an alternative, you can use ClientRegistrations.fromOidcIssuerLocation()
to only query the OpenID Connect Provider’s Configuration endpoint.
ClientRegistrationRepository
The ClientRegistrationRepository
serves as a repository for OAuth 2.0 / OpenID Connect 1.0 ClientRegistration
(s).
Client registration information is ultimately stored and owned by the associated Authorization Server. This repository provides the ability to retrieve a sub-set of the primary client registration information, which is stored with the Authorization Server. |
Spring Boot 2.x auto-configuration binds each of the properties under spring.security.oauth2.client.registration.[registrationId]
to an instance of ClientRegistration
and then composes each of the ClientRegistration
instance(s) within a ClientRegistrationRepository
.
The default implementation of ClientRegistrationRepository is InMemoryClientRegistrationRepository .
|
The auto-configuration also registers the ClientRegistrationRepository
as a @Bean
in the ApplicationContext
so that it is available for dependency-injection, if needed by the application.
The following listing shows an example:
@Controller
public class OAuth2ClientController {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@GetMapping("/")
public String index() {
ClientRegistration oktaRegistration =
this.clientRegistrationRepository.findByRegistrationId("okta");
...
return "index";
}
}
@Controller
class OAuth2ClientController {
@Autowired
private lateinit var clientRegistrationRepository: ClientRegistrationRepository
@GetMapping("/")
fun index(): String {
val oktaRegistration =
this.clientRegistrationRepository.findByRegistrationId("okta")
//...
return "index";
}
}
OAuth2AuthorizedClient
OAuth2AuthorizedClient
is a representation of an Authorized Client.
A client is considered to be authorized when the end-user (Resource Owner) has granted authorization to the client to access its protected resources.
OAuth2AuthorizedClient
serves the purpose of associating an OAuth2AccessToken
(and optional OAuth2RefreshToken
) to a ClientRegistration
(client) and resource owner, who is the Principal
end-user that granted the authorization.
OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService
OAuth2AuthorizedClientRepository
is responsible for persisting OAuth2AuthorizedClient
(s) between web requests.
Whereas, the primary role of OAuth2AuthorizedClientService
is to manage OAuth2AuthorizedClient
(s) at the application-level.
From a developer perspective, the OAuth2AuthorizedClientRepository
or OAuth2AuthorizedClientService
provides the capability to lookup an OAuth2AccessToken
associated with a client so that it may be used to initiate a protected resource request.
The following listing shows an example:
@Controller
public class OAuth2ClientController {
@Autowired
private OAuth2AuthorizedClientService authorizedClientService;
@GetMapping("/")
public String index(Authentication authentication) {
OAuth2AuthorizedClient authorizedClient =
this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName());
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}
@Controller
class OAuth2ClientController {
@Autowired
private lateinit var authorizedClientService: OAuth2AuthorizedClientService
@GetMapping("/")
fun index(authentication: Authentication): String {
val authorizedClient: OAuth2AuthorizedClient =
this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName());
val accessToken = authorizedClient.accessToken
...
return "index";
}
}
Spring Boot 2.x auto-configuration registers an OAuth2AuthorizedClientRepository and/or OAuth2AuthorizedClientService @Bean in the ApplicationContext .
However, the application may choose to override and register a custom OAuth2AuthorizedClientRepository or OAuth2AuthorizedClientService @Bean .
|
The default implementation of OAuth2AuthorizedClientService
is InMemoryOAuth2AuthorizedClientService
, which stores OAuth2AuthorizedClient
(s) in-memory.
Alternatively, the JDBC implementation JdbcOAuth2AuthorizedClientService
may be configured for persisting OAuth2AuthorizedClient
(s) in a database.
JdbcOAuth2AuthorizedClientService depends on the table definition described in OAuth 2.0 Client Schema.
|
OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider
The OAuth2AuthorizedClientManager
is responsible for the overall management of OAuth2AuthorizedClient
(s).
The primary responsibilities include:
-
Authorizing (or re-authorizing) an OAuth 2.0 Client, using an
OAuth2AuthorizedClientProvider
. -
Delegating the persistence of an
OAuth2AuthorizedClient
, typically using anOAuth2AuthorizedClientService
orOAuth2AuthorizedClientRepository
. -
Delegating to an
OAuth2AuthorizationSuccessHandler
when an OAuth 2.0 Client has been successfully authorized (or re-authorized). -
Delegating to an
OAuth2AuthorizationFailureHandler
when an OAuth 2.0 Client fails to authorize (or re-authorize).
An OAuth2AuthorizedClientProvider
implements a strategy for authorizing (or re-authorizing) an OAuth 2.0 Client.
Implementations will typically implement an authorization grant type, eg. authorization_code
, client_credentials
, etc.
The default implementation of OAuth2AuthorizedClientManager
is DefaultOAuth2AuthorizedClientManager
, which is associated with an OAuth2AuthorizedClientProvider
that may support multiple authorization grant types using a delegation-based composite.
The OAuth2AuthorizedClientProviderBuilder
may be used to configure and build the delegation-based composite.
The following code shows an example of how to configure and build an OAuth2AuthorizedClientProvider
composite that provides support for the authorization_code
, refresh_token
, client_credentials
and password
authorization grant types:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ClientRegistrationRepository,
authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager {
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
When an authorization attempt succeeds, the DefaultOAuth2AuthorizedClientManager
will delegate to the OAuth2AuthorizationSuccessHandler
, which (by default) will save the OAuth2AuthorizedClient
via the OAuth2AuthorizedClientRepository
.
In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved OAuth2AuthorizedClient
will be removed from the OAuth2AuthorizedClientRepository
via the RemoveAuthorizedClientOAuth2AuthorizationFailureHandler
.
The default behaviour may be customized via setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)
and setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)
.
The DefaultOAuth2AuthorizedClientManager
is also associated with a contextAttributesMapper
of type Function<OAuth2AuthorizeRequest, Map<String, Object>>
, which is responsible for mapping attribute(s) from the OAuth2AuthorizeRequest
to a Map
of attributes to be associated to the OAuth2AuthorizationContext
.
This can be useful when you need to supply an OAuth2AuthorizedClientProvider
with required (supported) attribute(s), eg. the PasswordOAuth2AuthorizedClientProvider
requires the resource owner’s username
and password
to be available in OAuth2AuthorizationContext.getAttributes()
.
The following code shows an example of the contextAttributesMapper
:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
// map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// `PasswordOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ClientRegistrationRepository,
authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager {
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken()
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
// Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
// map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper())
return authorizedClientManager
}
private fun contextAttributesMapper(): Function<OAuth2AuthorizeRequest, MutableMap<String, Any>> {
return Function { authorizeRequest ->
var contextAttributes: MutableMap<String, Any> = mutableMapOf()
val servletRequest: HttpServletRequest = authorizeRequest.getAttribute(HttpServletRequest::class.java.name)
val username: String = servletRequest.getParameter(OAuth2ParameterNames.USERNAME)
val password: String = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD)
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = hashMapOf()
// `PasswordOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username
contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password
}
contextAttributes
}
}
The DefaultOAuth2AuthorizedClientManager
is designed to be used within the context of a HttpServletRequest
.
When operating outside of a HttpServletRequest
context, use AuthorizedClientServiceOAuth2AuthorizedClientManager
instead.
A service application is a common use case for when to use an AuthorizedClientServiceOAuth2AuthorizedClientManager
.
Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account.
An OAuth 2.0 Client configured with the client_credentials
grant type can be considered a type of service application.
The following code shows an example of how to configure an AuthorizedClientServiceOAuth2AuthorizedClientManager
that provides support for the client_credentials
grant type:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ClientRegistrationRepository,
authorizedClientService: OAuth2AuthorizedClientService): OAuth2AuthorizedClientManager {
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build()
val authorizedClientManager = AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
12.2.2. Authorization Grant Support
Authorization Code
Please refer to the OAuth 2.0 Authorization Framework for further details on the Authorization Code grant. |
Obtaining Authorization
Please refer to the Authorization Request/Response protocol flow for the Authorization Code grant. |
Initiating the Authorization Request
The OAuth2AuthorizationRequestRedirectFilter
uses an OAuth2AuthorizationRequestResolver
to resolve an OAuth2AuthorizationRequest
and initiate the Authorization Code grant flow by redirecting the end-user’s user-agent to the Authorization Server’s Authorization Endpoint.
The primary role of the OAuth2AuthorizationRequestResolver
is to resolve an OAuth2AuthorizationRequest
from the provided web request.
The default implementation DefaultOAuth2AuthorizationRequestResolver
matches on the (default) path /oauth2/authorization/{registrationId}
extracting the registrationId
and using it to build the OAuth2AuthorizationRequest
for the associated ClientRegistration
.
Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/authorized/okta"
scope: read, write
provider:
okta:
authorization-uri: https://dev-1234.oktapreview.com/oauth2/v1/authorize
token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
A request with the base path /oauth2/authorization/okta
will initiate the Authorization Request redirect by the OAuth2AuthorizationRequestRedirectFilter
and ultimately start the Authorization Code grant flow.
The AuthorizationCodeOAuth2AuthorizedClientProvider is an implementation of OAuth2AuthorizedClientProvider for the Authorization Code grant,
which also initiates the Authorization Request redirect by the OAuth2AuthorizationRequestRedirectFilter .
|
If the OAuth 2.0 Client is a Public Client, then configure the OAuth 2.0 Client registration as follows:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-authentication-method: none
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/authorized/okta"
...
Public Clients are supported using Proof Key for Code Exchange (PKCE). If the client is running in an untrusted environment (eg. native application or web browser-based application) and therefore incapable of maintaining the confidentiality of it’s credentials, PKCE will automatically be used when the following conditions are true:
-
client-secret
is omitted (or empty) -
client-authentication-method
is set to "none" (ClientAuthenticationMethod.NONE
)
The DefaultOAuth2AuthorizationRequestResolver
also supports URI
template variables for the redirect-uri
using UriComponentsBuilder
.
The following configuration uses all the supported URI
template variables:
spring:
security:
oauth2:
client:
registration:
okta:
...
redirect-uri: "{baseScheme}://{baseHost}{basePort}{basePath}/authorized/{registrationId}"
...
{baseUrl} resolves to {baseScheme}://{baseHost}{basePort}{basePath}
|
Configuring the redirect-uri
with URI
template variables is especially useful when the OAuth 2.0 Client is running behind a Proxy Server.
This ensures that the X-Forwarded-*
headers are used when expanding the redirect-uri
.
Customizing the Authorization Request
One of the primary use cases an OAuth2AuthorizationRequestResolver
can realize is the ability to customize the Authorization Request with additional parameters above the standard parameters defined in the OAuth 2.0 Authorization Framework.
For example, OpenID Connect defines additional OAuth 2.0 request parameters for the Authorization Code Flow extending from the standard parameters defined in the OAuth 2.0 Authorization Framework.
One of those extended parameters is the prompt
parameter.
OPTIONAL. Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are: none, login, consent, select_account |
The following example shows how to configure the DefaultOAuth2AuthorizationRequestResolver
with a Consumer<OAuth2AuthorizationRequest.Builder>
that customizes the Authorization Request for oauth2Login()
, by including the request parameter prompt=consent
.
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
.authorizationRequestResolver(
authorizationRequestResolver(this.clientRegistrationRepository)
)
)
);
}
private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository) {
DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization");
authorizationRequestResolver.setAuthorizationRequestCustomizer(
authorizationRequestCustomizer());
return authorizationRequestResolver;
}
private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
return customizer -> customizer
.additionalParameters(params -> params.put("prompt", "consent"));
}
}
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {
@Autowired
private lateinit var customClientRegistrationRepository: ClientRegistrationRepository
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2Login {
authorizationEndpoint {
authorizationRequestResolver = authorizationRequestResolver(customClientRegistrationRepository)
}
}
}
}
private fun authorizationRequestResolver(
clientRegistrationRepository: ClientRegistrationRepository?): OAuth2AuthorizationRequestResolver? {
val authorizationRequestResolver = DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization")
authorizationRequestResolver.setAuthorizationRequestCustomizer(
authorizationRequestCustomizer())
return authorizationRequestResolver
}
private fun authorizationRequestCustomizer(): Consumer<OAuth2AuthorizationRequest.Builder> {
return Consumer { customizer ->
customizer
.additionalParameters { params -> params["prompt"] = "consent" }
}
}
}
For the simple use case, where the additional request parameter is always the same for a specific provider, it may be added directly in the authorization-uri
property.
For example, if the value for the request parameter prompt
is always consent
for the provider okta
, than simply configure as follows:
spring:
security:
oauth2:
client:
provider:
okta:
authorization-uri: https://dev-1234.oktapreview.com/oauth2/v1/authorize?prompt=consent
The preceding example shows the common use case of adding a custom parameter on top of the standard parameters.
Alternatively, if your requirements are more advanced, you can take full control in building the Authorization Request URI by simply overriding the OAuth2AuthorizationRequest.authorizationRequestUri
property.
OAuth2AuthorizationRequest.Builder.build() constructs the OAuth2AuthorizationRequest.authorizationRequestUri , which represents the Authorization Request URI including all query parameters using the application/x-www-form-urlencoded format.
|
The following example shows a variation of authorizationRequestCustomizer()
from the preceding example, and instead overrides the OAuth2AuthorizationRequest.authorizationRequestUri
property.
private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
return customizer -> customizer
.authorizationRequestUri(uriBuilder -> uriBuilder
.queryParam("prompt", "consent").build());
}
private fun authorizationRequestCustomizer(): Consumer<OAuth2AuthorizationRequest.Builder> {
return Consumer { customizer: OAuth2AuthorizationRequest.Builder ->
customizer
.authorizationRequestUri { uriBuilder: UriBuilder ->
uriBuilder
.queryParam("prompt", "consent").build()
}
}
}
Storing the Authorization Request
The AuthorizationRequestRepository
is responsible for the persistence of the OAuth2AuthorizationRequest
from the time the Authorization Request is initiated to the time the Authorization Response is received (the callback).
The OAuth2AuthorizationRequest is used to correlate and validate the Authorization Response.
|
The default implementation of AuthorizationRequestRepository
is HttpSessionOAuth2AuthorizationRequestRepository
, which stores the OAuth2AuthorizationRequest
in the HttpSession
.
If you have a custom implementation of AuthorizationRequestRepository
, you may configure it as shown in the following example:
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client(oauth2 -> oauth2
.authorizationCodeGrant(codeGrant -> codeGrant
.authorizationRequestRepository(this.authorizationRequestRepository())
...
)
);
}
}
@EnableWebSecurity
class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Client {
authorizationCodeGrant {
authorizationRequestRepository = authorizationRequestRepository()
}
}
}
}
}
<http>
<oauth2-client>
<authorization-code-grant authorization-request-repository-ref="authorizationRequestRepository"/>
</oauth2-client>
</http>
Requesting an Access Token
Please refer to the Access Token Request/Response protocol flow for the Authorization Code grant. |
The default implementation of OAuth2AccessTokenResponseClient
for the Authorization Code grant is DefaultAuthorizationCodeTokenResponseClient
, which uses a RestOperations
for exchanging an authorization code for an access token at the Authorization Server’s Token Endpoint.
The DefaultAuthorizationCodeTokenResponseClient
is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response.
Customizing the Access Token Request
If you need to customize the pre-processing of the Token Request, you can provide DefaultAuthorizationCodeTokenResponseClient.setRequestEntityConverter()
with a custom Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>>
.
The default implementation OAuth2AuthorizationCodeGrantRequestEntityConverter
builds a RequestEntity
representation of a standard OAuth 2.0 Access Token Request.
However, providing a custom Converter
, would allow you to extend the standard Token Request and add custom parameter(s).
The custom Converter must return a valid RequestEntity representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider.
|
Customizing the Access Token Response
On the other end, if you need to customize the post-handling of the Token Response, you will need to provide DefaultAuthorizationCodeTokenResponseClient.setRestOperations()
with a custom configured RestOperations
.
The default RestOperations
is configured as follows:
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
val restTemplate = RestTemplate(listOf(
FormHttpMessageConverter(),
OAuth2AccessTokenResponseHttpMessageConverter()))
restTemplate.errorHandler = OAuth2ErrorResponseErrorHandler()
Spring MVC FormHttpMessageConverter is required as it’s used when sending the OAuth 2.0 Access Token Request.
|
OAuth2AccessTokenResponseHttpMessageConverter
is a HttpMessageConverter
for an OAuth 2.0 Access Token Response.
You can provide OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()
with a custom Converter<Map<String, String>, OAuth2AccessTokenResponse>
that is used for converting the OAuth 2.0 Access Token Response parameters to an OAuth2AccessTokenResponse
.
OAuth2ErrorResponseErrorHandler
is a ResponseErrorHandler
that can handle an OAuth 2.0 Error, eg. 400 Bad Request.
It uses an OAuth2ErrorHttpMessageConverter
for converting the OAuth 2.0 Error parameters to an OAuth2Error
.
Whether you customize DefaultAuthorizationCodeTokenResponseClient
or provide your own implementation of OAuth2AccessTokenResponseClient
, you’ll need to configure it as shown in the following example:
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client(oauth2 -> oauth2
.authorizationCodeGrant(codeGrant -> codeGrant
.accessTokenResponseClient(this.accessTokenResponseClient())
...
)
);
}
}
@EnableWebSecurity
class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2Client {
authorizationCodeGrant {
accessTokenResponseClient = accessTokenResponseClient()
}
}
}
}
}
<http>
<oauth2-client>
<authorization-code-grant access-token-response-client-ref="accessTokenResponseClient"/>
</oauth2-client>
</http>
Refresh Token
Please refer to the OAuth 2.0 Authorization Framework for further details on the Refresh Token. |
Refreshing an Access Token
Please refer to the Access Token Request/Response protocol flow for the Refresh Token grant. |
The default implementation of OAuth2AccessTokenResponseClient
for the Refresh Token grant is DefaultRefreshTokenTokenResponseClient
, which uses a RestOperations
when refreshing an access token at the Authorization Server’s Token Endpoint.
The DefaultRefreshTokenTokenResponseClient
is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response.
Customizing the Access Token Request
If you need to customize the pre-processing of the Token Request, you can provide DefaultRefreshTokenTokenResponseClient.setRequestEntityConverter()
with a custom Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>>
.
The default implementation OAuth2RefreshTokenGrantRequestEntityConverter
builds a RequestEntity
representation of a standard OAuth 2.0 Access Token Request.
However, providing a custom Converter
, would allow you to extend the standard Token Request and add custom parameter(s).
The custom Converter must return a valid RequestEntity representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider.
|
Customizing the Access Token Response
On the other end, if you need to customize the post-handling of the Token Response, you will need to provide DefaultRefreshTokenTokenResponseClient.setRestOperations()
with a custom configured RestOperations
.
The default RestOperations
is configured as follows:
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
val restTemplate = RestTemplate(listOf(
FormHttpMessageConverter(),
OAuth2AccessTokenResponseHttpMessageConverter()))
restTemplate.errorHandler = OAuth2ErrorResponseErrorHandler()
Spring MVC FormHttpMessageConverter is required as it’s used when sending the OAuth 2.0 Access Token Request.
|
OAuth2AccessTokenResponseHttpMessageConverter
is a HttpMessageConverter
for an OAuth 2.0 Access Token Response.
You can provide OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()
with a custom Converter<Map<String, String>, OAuth2AccessTokenResponse>
that is used for converting the OAuth 2.0 Access Token Response parameters to an OAuth2AccessTokenResponse
.
OAuth2ErrorResponseErrorHandler
is a ResponseErrorHandler
that can handle an OAuth 2.0 Error, eg. 400 Bad Request.
It uses an OAuth2ErrorHttpMessageConverter
for converting the OAuth 2.0 Error parameters to an OAuth2Error
.
Whether you customize DefaultRefreshTokenTokenResponseClient
or provide your own implementation of OAuth2AccessTokenResponseClient
, you’ll need to configure it as shown in the following example:
// Customize
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient = ...
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken(configurer -> configurer.accessTokenResponseClient(refreshTokenTokenResponseClient))
.build();
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Customize
val refreshTokenTokenResponseClient: OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> = ...
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken { it.accessTokenResponseClient(refreshTokenTokenResponseClient) }
.build()
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
OAuth2AuthorizedClientProviderBuilder.builder().refreshToken() configures a RefreshTokenOAuth2AuthorizedClientProvider ,
which is an implementation of an OAuth2AuthorizedClientProvider for the Refresh Token grant.
|
The OAuth2RefreshToken
may optionally be returned in the Access Token Response for the authorization_code
and password
grant types.
If the OAuth2AuthorizedClient.getRefreshToken()
is available and the OAuth2AuthorizedClient.getAccessToken()
is expired, it will automatically be refreshed by the RefreshTokenOAuth2AuthorizedClientProvider
.
Client Credentials
Please refer to the OAuth 2.0 Authorization Framework for further details on the Client Credentials grant. |
Requesting an Access Token
Please refer to the Access Token Request/Response protocol flow for the Client Credentials grant. |
The default implementation of OAuth2AccessTokenResponseClient
for the Client Credentials grant is DefaultClientCredentialsTokenResponseClient
, which uses a RestOperations
when requesting an access token at the Authorization Server’s Token Endpoint.
The DefaultClientCredentialsTokenResponseClient
is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response.
Customizing the Access Token Request
If you need to customize the pre-processing of the Token Request, you can provide DefaultClientCredentialsTokenResponseClient.setRequestEntityConverter()
with a custom Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>>
.
The default implementation OAuth2ClientCredentialsGrantRequestEntityConverter
builds a RequestEntity
representation of a standard OAuth 2.0 Access Token Request.
However, providing a custom Converter
, would allow you to extend the standard Token Request and add custom parameter(s).
The custom Converter must return a valid RequestEntity representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider.
|
Customizing the Access Token Response
On the other end, if you need to customize the post-handling of the Token Response, you will need to provide DefaultClientCredentialsTokenResponseClient.setRestOperations()
with a custom configured RestOperations
.
The default RestOperations
is configured as follows:
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
val restTemplate = RestTemplate(listOf(
FormHttpMessageConverter(),
OAuth2AccessTokenResponseHttpMessageConverter()))
restTemplate.errorHandler = OAuth2ErrorResponseErrorHandler()
Spring MVC FormHttpMessageConverter is required as it’s used when sending the OAuth 2.0 Access Token Request.
|
OAuth2AccessTokenResponseHttpMessageConverter
is a HttpMessageConverter
for an OAuth 2.0 Access Token Response.
You can provide OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()
with a custom Converter<Map<String, String>, OAuth2AccessTokenResponse>
that is used for converting the OAuth 2.0 Access Token Response parameters to an OAuth2AccessTokenResponse
.
OAuth2ErrorResponseErrorHandler
is a ResponseErrorHandler
that can handle an OAuth 2.0 Error, eg. 400 Bad Request.
It uses an OAuth2ErrorHttpMessageConverter
for converting the OAuth 2.0 Error parameters to an OAuth2Error
.
Whether you customize DefaultClientCredentialsTokenResponseClient
or provide your own implementation of OAuth2AccessTokenResponseClient
, you’ll need to configure it as shown in the following example:
// Customize
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient = ...
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(configurer -> configurer.accessTokenResponseClient(clientCredentialsTokenResponseClient))
.build();
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Customize
val clientCredentialsTokenResponseClient: OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> = ...
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials { it.accessTokenResponseClient(clientCredentialsTokenResponseClient) }
.build()
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials() configures a ClientCredentialsOAuth2AuthorizedClientProvider ,
which is an implementation of an OAuth2AuthorizedClientProvider for the Client Credentials grant.
|
Using the Access Token
Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
authorization-grant-type: client_credentials
scope: read, write
provider:
okta:
token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
…and the OAuth2AuthorizedClientManager
@Bean
:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ClientRegistrationRepository,
authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager {
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
You may obtain the OAuth2AccessToken
as follows:
@Controller
public class OAuth2ClientController {
@Autowired
private OAuth2AuthorizedClientManager authorizedClientManager;
@GetMapping("/")
public String index(Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
.principal(authentication)
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}
class OAuth2ClientController {
@Autowired
private lateinit var authorizedClientManager: OAuth2AuthorizedClientManager
@GetMapping("/")
fun index(authentication: Authentication?,
servletRequest: HttpServletRequest,
servletResponse: HttpServletResponse): String {
val authorizeRequest: OAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
.principal(authentication)
.attributes(Consumer { attrs: MutableMap<String, Any> ->
attrs[HttpServletRequest::class.java.name] = servletRequest
attrs[HttpServletResponse::class.java.name] = servletResponse
})
.build()
val authorizedClient = authorizedClientManager.authorize(authorizeRequest)
val accessToken: OAuth2AccessToken = authorizedClient.accessToken
...
return "index"
}
}
HttpServletRequest and HttpServletResponse are both OPTIONAL attributes.
If not provided, it will default to ServletRequestAttributes using RequestContextHolder.getRequestAttributes() .
|
Resource Owner Password Credentials
Please refer to the OAuth 2.0 Authorization Framework for further details on the Resource Owner Password Credentials grant. |
Requesting an Access Token
Please refer to the Access Token Request/Response protocol flow for the Resource Owner Password Credentials grant. |
The default implementation of OAuth2AccessTokenResponseClient
for the Resource Owner Password Credentials grant is DefaultPasswordTokenResponseClient
, which uses a RestOperations
when requesting an access token at the Authorization Server’s Token Endpoint.
The DefaultPasswordTokenResponseClient
is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response.
Customizing the Access Token Request
If you need to customize the pre-processing of the Token Request, you can provide DefaultPasswordTokenResponseClient.setRequestEntityConverter()
with a custom Converter<OAuth2PasswordGrantRequest, RequestEntity<?>>
.
The default implementation OAuth2PasswordGrantRequestEntityConverter
builds a RequestEntity
representation of a standard OAuth 2.0 Access Token Request.
However, providing a custom Converter
, would allow you to extend the standard Token Request and add custom parameter(s).
The custom Converter must return a valid RequestEntity representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider.
|
Customizing the Access Token Response
On the other end, if you need to customize the post-handling of the Token Response, you will need to provide DefaultPasswordTokenResponseClient.setRestOperations()
with a custom configured RestOperations
.
The default RestOperations
is configured as follows:
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
val restTemplate = RestTemplate(listOf(
FormHttpMessageConverter(),
OAuth2AccessTokenResponseHttpMessageConverter()))
restTemplate.errorHandler = OAuth2ErrorResponseErrorHandler()
Spring MVC FormHttpMessageConverter is required as it’s used when sending the OAuth 2.0 Access Token Request.
|
OAuth2AccessTokenResponseHttpMessageConverter
is a HttpMessageConverter
for an OAuth 2.0 Access Token Response.
You can provide OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()
with a custom Converter<Map<String, String>, OAuth2AccessTokenResponse>
that is used for converting the OAuth 2.0 Access Token Response parameters to an OAuth2AccessTokenResponse
.
OAuth2ErrorResponseErrorHandler
is a ResponseErrorHandler
that can handle an OAuth 2.0 Error, eg. 400 Bad Request.
It uses an OAuth2ErrorHttpMessageConverter
for converting the OAuth 2.0 Error parameters to an OAuth2Error
.
Whether you customize DefaultPasswordTokenResponseClient
or provide your own implementation of OAuth2AccessTokenResponseClient
, you’ll need to configure it as shown in the following example:
// Customize
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient = ...
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.password(configurer -> configurer.accessTokenResponseClient(passwordTokenResponseClient))
.refreshToken()
.build();
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
val passwordTokenResponseClient: OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> = ...
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.password { it.accessTokenResponseClient(passwordTokenResponseClient) }
.refreshToken()
.build()
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
OAuth2AuthorizedClientProviderBuilder.builder().password() configures a PasswordOAuth2AuthorizedClientProvider ,
which is an implementation of an OAuth2AuthorizedClientProvider for the Resource Owner Password Credentials grant.
|
Using the Access Token
Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
authorization-grant-type: password
scope: read, write
provider:
okta:
token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
…and the OAuth2AuthorizedClientManager
@Bean
:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
// map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// `PasswordOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ClientRegistrationRepository,
authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager {
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken()
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
// Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
// map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper())
return authorizedClientManager
}
private fun contextAttributesMapper(): Function<OAuth2AuthorizeRequest, MutableMap<String, Any>> {
return Function { authorizeRequest ->
var contextAttributes: MutableMap<String, Any> = mutableMapOf()
val servletRequest: HttpServletRequest = authorizeRequest.getAttribute(HttpServletRequest::class.java.name)
val username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME)
val password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD)
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = hashMapOf()
// `PasswordOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username
contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password
}
contextAttributes
}
}
You may obtain the OAuth2AccessToken
as follows:
@Controller
public class OAuth2ClientController {
@Autowired
private OAuth2AuthorizedClientManager authorizedClientManager;
@GetMapping("/")
public String index(Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
.principal(authentication)
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}
@Controller
class OAuth2ClientController {
@Autowired
private lateinit var authorizedClientManager: OAuth2AuthorizedClientManager
@GetMapping("/")
fun index(authentication: Authentication?,
servletRequest: HttpServletRequest,
servletResponse: HttpServletResponse): String {
val authorizeRequest: OAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
.principal(authentication)
.attributes(Consumer {
it[HttpServletRequest::class.java.name] = servletRequest
it[HttpServletResponse::class.java.name] = servletResponse
})
.build()
val authorizedClient = authorizedClientManager.authorize(authorizeRequest)
val accessToken: OAuth2AccessToken = authorizedClient.accessToken
...
return "index"
}
}
HttpServletRequest and HttpServletResponse are both OPTIONAL attributes.
If not provided, it will default to ServletRequestAttributes using RequestContextHolder.getRequestAttributes() .
|
JWT Bearer
Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on the JWT Bearer grant. |
Requesting an Access Token
Please refer to the Access Token Request/Response protocol flow for the JWT Bearer grant. |
The default implementation of OAuth2AccessTokenResponseClient
for the JWT Bearer grant is DefaultJwtBearerTokenResponseClient
, which uses a RestOperations
when requesting an access token at the Authorization Server’s Token Endpoint.
The DefaultJwtBearerTokenResponseClient
is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response.
Customizing the Access Token Request
If you need to customize the pre-processing of the Token Request, you can provide DefaultJwtBearerTokenResponseClient.setRequestEntityConverter()
with a custom Converter<JwtBearerGrantRequest, RequestEntity<?>>
.
The default implementation JwtBearerGrantRequestEntityConverter
builds a RequestEntity
representation of a OAuth 2.0 Access Token Request.
However, providing a custom Converter
, would allow you to extend the Token Request and add custom parameter(s).
Customizing the Access Token Response
On the other end, if you need to customize the post-handling of the Token Response, you will need to provide DefaultJwtBearerTokenResponseClient.setRestOperations()
with a custom configured RestOperations
.
The default RestOperations
is configured as follows:
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
val restTemplate = RestTemplate(listOf(
FormHttpMessageConverter(),
OAuth2AccessTokenResponseHttpMessageConverter()))
restTemplate.errorHandler = OAuth2ErrorResponseErrorHandler()
Spring MVC FormHttpMessageConverter is required as it’s used when sending the OAuth 2.0 Access Token Request.
|
OAuth2AccessTokenResponseHttpMessageConverter
is a HttpMessageConverter
for an OAuth 2.0 Access Token Response.
You can provide OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()
with a custom Converter<Map<String, String>, OAuth2AccessTokenResponse>
that is used for converting the OAuth 2.0 Access Token Response parameters to an OAuth2AccessTokenResponse
.
OAuth2ErrorResponseErrorHandler
is a ResponseErrorHandler
that can handle an OAuth 2.0 Error, eg. 400 Bad Request.
It uses an OAuth2ErrorHttpMessageConverter
for converting the OAuth 2.0 Error parameters to an OAuth2Error
.
Whether you customize DefaultJwtBearerTokenResponseClient
or provide your own implementation of OAuth2AccessTokenResponseClient
, you’ll need to configure it as shown in the following example:
// Customize
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerTokenResponseClient = ...
JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerTokenResponseClient);
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.provider(jwtBearerAuthorizedClientProvider)
.build();
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Customize
val jwtBearerTokenResponseClient: OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> = ...
val jwtBearerAuthorizedClientProvider = JwtBearerOAuth2AuthorizedClientProvider()
jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerTokenResponseClient);
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.provider(jwtBearerAuthorizedClientProvider)
.build()
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
Using the Access Token
Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
authorization-grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer
scope: read
provider:
okta:
token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
…and the OAuth2AuthorizedClientManager
@Bean
:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider =
new JwtBearerOAuth2AuthorizedClientProvider();
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.provider(jwtBearerAuthorizedClientProvider)
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ClientRegistrationRepository,
authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager {
val jwtBearerAuthorizedClientProvider = JwtBearerOAuth2AuthorizedClientProvider()
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.provider(jwtBearerAuthorizedClientProvider)
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
You may obtain the OAuth2AccessToken
as follows:
@RestController
public class OAuth2ResourceServerController {
@Autowired
private OAuth2AuthorizedClientManager authorizedClientManager;
@GetMapping("/resource")
public String resource(JwtAuthenticationToken jwtAuthentication) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
.principal(jwtAuthentication)
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
}
}
class OAuth2ResourceServerController {
@Autowired
private lateinit var authorizedClientManager: OAuth2AuthorizedClientManager
@GetMapping("/resource")
fun resource(jwtAuthentication: JwtAuthenticationToken?): String {
val authorizeRequest: OAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
.principal(jwtAuthentication)
.build()
val authorizedClient = authorizedClientManager.authorize(authorizeRequest)
val accessToken: OAuth2AccessToken = authorizedClient.accessToken
...
}
}
12.2.3. Client Authentication Support
JWT Bearer
Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on JWT Bearer Client Authentication. |
The default implementation for JWT Bearer Client Authentication is NimbusJwtClientAuthenticationParametersConverter
,
which is a Converter
that customizes the Token Request parameters by adding
a signed JSON Web Token (JWS) in the client_assertion
parameter.
The java.security.PrivateKey
or javax.crypto.SecretKey
used for signing the JWS
is supplied by the com.nimbusds.jose.jwk.JWK
resolver associated with NimbusJwtClientAuthenticationParametersConverter
.
Authenticate using private_key_jwt
Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-authentication-method: private_key_jwt
authorization-grant-type: authorization_code
...
The following example shows how to configure DefaultAuthorizationCodeTokenResponseClient
:
Function<ClientRegistration, JWK> jwkResolver = (clientRegistration) -> {
if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
// Assuming RSA key type
RSAPublicKey publicKey = ...
RSAPrivateKey privateKey = ...
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
return null;
};
OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter =
new OAuth2AuthorizationCodeGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver));
DefaultAuthorizationCodeTokenResponseClient tokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
tokenResponseClient.setRequestEntityConverter(requestEntityConverter);
val jwkResolver: Function<ClientRegistration, JWK> =
Function<ClientRegistration, JWK> { clientRegistration ->
if (clientRegistration.clientAuthenticationMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
// Assuming RSA key type
var publicKey: RSAPublicKey
var privateKey: RSAPrivateKey
RSAKey.Builder(publicKey) = //...
.privateKey(privateKey) = //...
.keyID(UUID.randomUUID().toString())
.build()
}
null
}
val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter()
requestEntityConverter.addParametersConverter(
NimbusJwtClientAuthenticationParametersConverter(jwkResolver)
)
val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
tokenResponseClient.setRequestEntityConverter(requestEntityConverter)
Authenticate using client_secret_jwt
Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
client-authentication-method: client_secret_jwt
authorization-grant-type: client_credentials
...
The following example shows how to configure DefaultClientCredentialsTokenResponseClient
:
Function<ClientRegistration, JWK> jwkResolver = (clientRegistration) -> {
if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
SecretKeySpec secretKey = new SecretKeySpec(
clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8),
"HmacSHA256");
return new OctetSequenceKey.Builder(secretKey)
.keyID(UUID.randomUUID().toString())
.build();
}
return null;
};
OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter =
new OAuth2ClientCredentialsGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver));
DefaultClientCredentialsTokenResponseClient tokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
tokenResponseClient.setRequestEntityConverter(requestEntityConverter);
val jwkResolver = Function<ClientRegistration, JWK?> { clientRegistration: ClientRegistration ->
if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) {
val secretKey = SecretKeySpec(
clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8),
"HmacSHA256"
)
OctetSequenceKey.Builder(secretKey)
.keyID(UUID.randomUUID().toString())
.build()
}
null
}
val requestEntityConverter = OAuth2ClientCredentialsGrantRequestEntityConverter()
requestEntityConverter.addParametersConverter(
NimbusJwtClientAuthenticationParametersConverter(jwkResolver)
)
val tokenResponseClient = DefaultClientCredentialsTokenResponseClient()
tokenResponseClient.setRequestEntityConverter(requestEntityConverter)
12.2.4. Additional Features
Resolving an Authorized Client
The @RegisteredOAuth2AuthorizedClient
annotation provides the capability of resolving a method parameter to an argument value of type OAuth2AuthorizedClient
.
This is a convenient alternative compared to accessing the OAuth2AuthorizedClient
using the OAuth2AuthorizedClientManager
or OAuth2AuthorizedClientService
.
@Controller
public class OAuth2ClientController {
@GetMapping("/")
public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}
@Controller
class OAuth2ClientController {
@GetMapping("/")
fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String {
val accessToken = authorizedClient.accessToken
...
return "index"
}
}
The @RegisteredOAuth2AuthorizedClient
annotation is handled by OAuth2AuthorizedClientArgumentResolver
, which directly uses an OAuth2AuthorizedClientManager and therefore inherits it’s capabilities.
12.2.5. WebClient integration for Servlet Environments
The OAuth 2.0 Client support integrates with WebClient
using an ExchangeFilterFunction
.
The ServletOAuth2AuthorizedClientExchangeFilterFunction
provides a simple mechanism for requesting protected resources by using an OAuth2AuthorizedClient
and including the associated OAuth2AccessToken
as a Bearer Token.
It directly uses an OAuth2AuthorizedClientManager and therefore inherits the following capabilities:
-
An
OAuth2AccessToken
will be requested if the client has not yet been authorized.-
authorization_code
- triggers the Authorization Request redirect to initiate the flow -
client_credentials
- the access token is obtained directly from the Token Endpoint -
password
- the access token is obtained directly from the Token Endpoint
-
-
If the
OAuth2AccessToken
is expired, it will be refreshed (or renewed) if anOAuth2AuthorizedClientProvider
is available to perform the authorization
The following code shows an example of how to configure WebClient
with OAuth 2.0 Client support:
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient {
val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build()
}
Providing the Authorized Client
The ServletOAuth2AuthorizedClientExchangeFilterFunction
determines the client to use (for a request) by resolving the OAuth2AuthorizedClient
from the ClientRequest.attributes()
(request attributes).
The following code shows how to set an OAuth2AuthorizedClient
as a request attribute:
@GetMapping("/")
public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
String resourceUri = ...
String body = webClient
.get()
.uri(resourceUri)
.attributes(oauth2AuthorizedClient(authorizedClient)) (1)
.retrieve()
.bodyToMono(String.class)
.block();
...
return "index";
}
@GetMapping("/")
fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String {
val resourceUri: String = ...
val body: String = webClient
.get()
.uri(resourceUri)
.attributes(oauth2AuthorizedClient(authorizedClient)) (1)
.retrieve()
.bodyToMono()
.block()
...
return "index"
}
1 | oauth2AuthorizedClient() is a static method in ServletOAuth2AuthorizedClientExchangeFilterFunction . |
The following code shows how to set the ClientRegistration.getRegistrationId()
as a request attribute:
@GetMapping("/")
public String index() {
String resourceUri = ...
String body = webClient
.get()
.uri(resourceUri)
.attributes(clientRegistrationId("okta")) (1)
.retrieve()
.bodyToMono(String.class)
.block();
...
return "index";
}
@GetMapping("/")
fun index(): String {
val resourceUri: String = ...
val body: String = webClient
.get()
.uri(resourceUri)
.attributes(clientRegistrationId("okta")) (1)
.retrieve()
.bodyToMono()
.block()
...
return "index"
}
1 | clientRegistrationId() is a static method in ServletOAuth2AuthorizedClientExchangeFilterFunction . |
Defaulting the Authorized Client
If neither OAuth2AuthorizedClient
or ClientRegistration.getRegistrationId()
is provided as a request attribute, the ServletOAuth2AuthorizedClientExchangeFilterFunction
can determine the default client to use depending on it’s configuration.
If setDefaultOAuth2AuthorizedClient(true)
is configured and the user has authenticated using HttpSecurity.oauth2Login()
, the OAuth2AccessToken
associated with the current OAuth2AuthenticationToken
is used.
The following code shows the specific configuration:
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient {
val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
oauth2Client.setDefaultOAuth2AuthorizedClient(true)
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build()
}
It is recommended to be cautious with this feature since all HTTP requests will receive the access token. |
Alternatively, if setDefaultClientRegistrationId("okta")
is configured with a valid ClientRegistration
, the OAuth2AccessToken
associated with the OAuth2AuthorizedClient
is used.
The following code shows the specific configuration:
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("okta");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient {
val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
oauth2Client.setDefaultClientRegistrationId("okta")
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build()
}
It is recommended to be cautious with this feature since all HTTP requests will receive the access token. |
12.3. OAuth 2.0 Resource Server
Spring Security supports protecting endpoints using two forms of OAuth 2.0 Bearer Tokens:
-
Opaque Tokens
This is handy in circumstances where an application has delegated its authority management to an authorization server (for example, Okta or Ping Identity). This authorization server can be consulted by resource servers to authorize requests.
This section provides details on how Spring Security provides support for OAuth 2.0 Bearer Tokens.
Working samples for both JWTs and Opaque Tokens are available in the Spring Security Samples repository. |
Let’s take a look at how Bearer Token Authentication works within Spring Security. First, we see that, like Basic Authentication, the WWW-Authenticate header is sent back to an unauthenticated client.
The figure above builds off our SecurityFilterChain
diagram.
First, a user makes an unauthenticated request to the resource /private
for which it is not authorized.
Spring Security’s FilterSecurityInterceptor
indicates that the unauthenticated request is Denied by throwing an AccessDeniedException
.
Since the user is not authenticated, ExceptionTranslationFilter
initiates Start Authentication.
The configured AuthenticationEntryPoint
is an instance of BearerTokenAuthenticationEntryPoint
which sends a WWW-Authenticate header.
The RequestCache
is typically a NullRequestCache
that does not save the request since the client is capable of replaying the requests it originally requested.
When a client receives the WWW-Authenticate: Bearer
header, it knows it should retry with a bearer token.
Below is the flow for the bearer token being processed.
The figure builds off our SecurityFilterChain
diagram.
When the user submits their bearer token, the BearerTokenAuthenticationFilter
creates a BearerTokenAuthenticationToken
which is a type of Authentication
by extracting the token from the HttpServletRequest
.
Next, the HttpServletRequest
is passed to the AuthenticationManagerResolver
, which selects the AuthenticationManager
. The BearerTokenAuthenticationToken
is passed into the AuthenticationManager
to be authenticated.
The details of what AuthenticationManager
looks like depends on whether you’re configured for JWT or opaque token.
If authentication fails, then Failure
-
The SecurityContextHolder is cleared out.
-
The
AuthenticationEntryPoint
is invoked to trigger the WWW-Authenticate header to be sent again.
If authentication is successful, then Success.
-
The Authentication is set on the SecurityContextHolder.
-
The
BearerTokenAuthenticationFilter
invokesFilterChain.doFilter(request,response)
to continue with the rest of the application logic.
12.3.1. Minimal Dependencies for JWT
Most Resource Server support is collected into spring-security-oauth2-resource-server
.
However, the support for decoding and verifying JWTs is in spring-security-oauth2-jose
, meaning that both are necessary in order to have a working resource server that supports JWT-encoded Bearer Tokens.
12.3.2. Minimal Configuration for JWTs
When using Spring Boot, configuring an application as a resource server consists of two basic steps. First, include the needed dependencies and second, indicate the location of the authorization server.
Specifying the Authorization Server
In a Spring Boot application, to specify which authorization server to use, simply do:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
Where https://idp.example.com/issuer
is the value contained in the iss
claim for JWT tokens that the authorization server will issue.
Resource Server will use this property to further self-configure, discover the authorization server’s public keys, and subsequently validate incoming JWTs.
To use the issuer-uri property, it must also be true that one of https://idp.example.com/issuer/.well-known/openid-configuration , https://idp.example.com/.well-known/openid-configuration/issuer , or https://idp.example.com/.well-known/oauth-authorization-server/issuer is a supported endpoint for the authorization server.
This endpoint is referred to as a Provider Configuration endpoint or a Authorization Server Metadata endpoint.
|
And that’s it!
Startup Expectations
When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
It achieves this through a deterministic startup process:
-
Query the Provider Configuration or Authorization Server Metadata endpoint for the
jwks_url
property -
Query the
jwks_url
endpoint for supported algorithms -
Configure the validation strategy to query
jwks_url
for valid public keys of the algorithms found -
Configure the validation strategy to validate each JWTs
iss
claim againsthttps://idp.example.com
.
A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail. |
Runtime Expectations
Once the application is started up, Resource Server will attempt to process any request containing an Authorization: Bearer
header:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
Given a well-formed JWT, Resource Server will:
-
Validate its signature against a public key obtained from the
jwks_url
endpoint during startup and matched against the JWT -
Validate the JWT’s
exp
andnbf
timestamps and the JWT’siss
claim, and -
Map each scope to an authority with the prefix
SCOPE_
.
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate JWTs. |
The resulting Authentication#getPrincipal
, by default, is a Spring Security Jwt
object, and Authentication#getName
maps to the JWT’s sub
property, if one is present.
From here, consider jumping to:
12.3.3. How JWT Authentication Works
Next, let’s see the architectural components that Spring Security uses to support JWT Authentication in servlet-based applications, like the one we just saw.
JwtAuthenticationProvider
is an AuthenticationProvider
implementation that leverages a JwtDecoder
and JwtAuthenticationConverter
to authenticate a JWT.
Let’s take a look at how JwtAuthenticationProvider
works within Spring Security.
The figure explains details of how the AuthenticationManager
in figures from Reading the Bearer Token works.
JwtAuthenticationProvider
Usage The authentication Filter
from Reading the Bearer Token passes a BearerTokenAuthenticationToken
to the AuthenticationManager
which is implemented by ProviderManager
.
The ProviderManager
is configured to use an AuthenticationProvider of type JwtAuthenticationProvider
.
JwtAuthenticationProvider
decodes, verifies, and validates the Jwt
using a JwtDecoder
.
JwtAuthenticationProvider
then uses the JwtAuthenticationConverter
to convert the Jwt
into a Collection
of granted authorities.
When authentication is successful, the Authentication
that is returned is of type JwtAuthenticationToken
and has a principal that is the Jwt
returned by the configured JwtDecoder
.
Ultimately, the returned JwtAuthenticationToken
will be set on the SecurityContextHolder
by the authentication Filter
.
12.3.4. Specifying the Authorization Server JWK Set Uri Directly
If the authorization server doesn’t support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, then the jwk-set-uri
can be supplied as well:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
The JWK Set uri is not standardized, but can typically be found in the authorization server’s documentation |
Consequently, Resource Server will not ping the authorization server at startup.
We still specify the issuer-uri
so that Resource Server still validates the iss
claim on incoming JWTs.
This property can also be supplied directly on the DSL. |
12.3.5. Overriding or Replacing Boot Auto Configuration
There are two @Bean
s that Spring Boot generates on Resource Server’s behalf.
The first is a WebSecurityConfigurerAdapter
that configures the app as a resource server. When including spring-security-oauth2-jose
, this WebSecurityConfigurerAdapter
looks like:
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
If the application doesn’t expose a WebSecurityConfigurerAdapter
bean, then Spring Boot will expose the above default one.
Replacing this is as simple as exposing the bean within the application:
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(myConverter())
)
);
}
}
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("SCOPE_message:read"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = myConverter()
}
}
}
}
}
The above requires the scope of message:read
for any URL that starts with /messages/
.
Methods on the oauth2ResourceServer
DSL will also override or replace auto configuration.
For example, the second @Bean
Spring Boot creates is a JwtDecoder
, which decodes String
tokens into validated instances of Jwt
:
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): JwtDecoder {
return JwtDecoders.fromIssuerLocation(issuerUri)
}
Calling JwtDecoders#fromIssuerLocation is what invokes the Provider Configuration or Authorization Server Metadata endpoint in order to derive the JWK Set Uri.
|
If the application doesn’t expose a JwtDecoder
bean, then Spring Boot will expose the above default one.
And its configuration can be overridden using jwkSetUri()
or replaced using decoder()
.
Or, if you’re not using Spring Boot at all, then both of these components - the filter chain and a JwtDecoder
can be specified in XML.
The filter chain is specified like so:
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<jwt decoder-ref="jwtDecoder"/>
</oauth2-resource-server>
</http>
And the JwtDecoder
like so:
<bean id="jwtDecoder"
class="org.springframework.security.oauth2.jwt.JwtDecoders"
factory-method="fromIssuerLocation">
<constructor-arg value="${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}"/>
</bean>
Using jwkSetUri()
An authorization server’s JWK Set Uri can be configured as a configuration property or it can be supplied in the DSL:
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwkSetUri("https://idp.example.com/.well-known/jwks.json")
)
);
}
}
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
}
}
}
}
}
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<jwt jwk-set-uri="https://idp.example.com/.well-known/jwks.json"/>
</oauth2-resource-server>
</http>
Using jwkSetUri()
takes precedence over any configuration property.
Using decoder()
More powerful than jwkSetUri()
is decoder()
, which will completely replace any Boot auto configuration of JwtDecoder
:
@EnableWebSecurity
public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(myCustomDecoder())
)
);
}
}
@EnableWebSecurity
class DirectlyConfiguredJwtDecoder : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtDecoder = myCustomDecoder()
}
}
}
}
}
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<jwt decoder-ref="myCustomDecoder"/>
</oauth2-resource-server>
</http>
This is handy when deeper configuration, like validation, mapping, or request timeouts, is necessary.
Exposing a JwtDecoder
@Bean
Or, exposing a JwtDecoder
@Bean
has the same effect as decoder()
:
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
}
12.3.6. Configuring Trusted Algorithms
By default, NimbusJwtDecoder
, and hence Resource Server, will only trust and verify tokens using RS256
.
You can customize this via Spring Boot, the NimbusJwtDecoder builder, or from the JWK Set response.
Via Spring Boot
The simplest way to set the algorithm is as a property:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithm: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
Using a Builder
For greater power, though, we can use a builder that ships with NimbusJwtDecoder
:
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build()
}
Calling jwsAlgorithm
more than once will configure NimbusJwtDecoder
to trust more than one algorithm, like so:
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
Or, you can call jwsAlgorithms
:
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}.build()
}
From JWK Set response
Since Spring Security’s JWT support is based off of Nimbus, you can use all it’s great features as well.
For example, Nimbus has a JWSKeySelector
implementation that will select the set of algorithms based on the JWK Set URI response.
You can use it to generate a NimbusJwtDecoder
like so:
@Bean
public JwtDecoder jwtDecoder() {
// makes a request to the JWK Set endpoint
JWSKeySelector<SecurityContext> jwsKeySelector =
JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl);
DefaultJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
return new NimbusJwtDecoder(jwtProcessor);
}
@Bean
fun jwtDecoder(): JwtDecoder {
// makes a request to the JWK Set endpoint
val jwsKeySelector: JWSKeySelector<SecurityContext> = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL<SecurityContext>(this.jwkSetUrl)
val jwtProcessor: DefaultJWTProcessor<SecurityContext> = DefaultJWTProcessor()
jwtProcessor.jwsKeySelector = jwsKeySelector
return NimbusJwtDecoder(jwtProcessor)
}
12.3.7. Trusting a Single Asymmetric Key
Simpler than backing a Resource Server with a JWK Set endpoint is to hard-code an RSA public key. The public key can be provided via Spring Boot or by Using a Builder.
Via Spring Boot
Specifying a key via Spring Boot is quite simple. The key’s location can be specified like so:
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:my-key.pub
Or, to allow for a more sophisticated lookup, you can post-process the RsaKeyConversionServicePostProcessor
:
@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
return beanFactory ->
beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
.setResourceLoader(new CustomResourceLoader());
}
@Bean
fun conversionServiceCustomizer(): BeanFactoryPostProcessor {
return BeanFactoryPostProcessor { beanFactory ->
beanFactory.getBean<RsaKeyConversionServicePostProcessor>()
.setResourceLoader(CustomResourceLoader())
}
}
Specify your key’s location:
key.location: hfds://my-key.pub
And then autowire the value:
@Value("${key.location}")
RSAPublicKey key;
@Value("\${key.location}")
val key: RSAPublicKey? = null
Using a Builder
To wire an RSAPublicKey
directly, you can simply use the appropriate NimbusJwtDecoder
builder, like so:
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withPublicKey(this.key).build()
}
12.3.8. Trusting a Single Symmetric Key
Using a single symmetric key is also simple.
You can simply load in your SecretKey
and use the appropriate NimbusJwtDecoder
builder, like so:
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withSecretKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withSecretKey(key).build()
}
12.3.9. Configuring Authorization
A JWT that is issued from an OAuth 2.0 Authorization Server will typically either have a scope
or scp
attribute, indicating the scopes (or authorities) it’s been granted, for example:
{ …, "scope" : "messages contacts"}
When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".
This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix:
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
}
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
authorize("/messages/**", hasAuthority("SCOPE_messages"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
}
<http>
<intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
<intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
<oauth2-resource-server>
<jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"/>
</oauth2-resource-server>
</http>
Or similarly with method security:
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): List<Message> { }
Extracting Authorities Manually
However, there are a number of circumstances where this default is insufficient.
For example, some authorization servers don’t use the scope
attribute, but instead have their own custom attribute.
Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities.
To this end, Spring Security ships with JwtAuthenticationConverter
, which is responsible for converting a Jwt
into an Authentication
.
By default, Spring Security will wire the JwtAuthenticationProvider
with a default instance of JwtAuthenticationConverter
.
As part of configuring a JwtAuthenticationConverter
, you can supply a subsidiary converter to go from Jwt
to a Collection
of granted authorities.
Let’s say that that your authorization server communicates authorities in a custom claim called authorities
.
In that case, you can configure the claim that JwtAuthenticationConverter
should inspect, like so:
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities")
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
return jwtAuthenticationConverter
}
<http>
<intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
<intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
<oauth2-resource-server>
<jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
</oauth2-resource-server>
</http>
<bean id="jwtAuthenticationConverter"
class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
<property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>
<bean id="jwtGrantedAuthoritiesConverter"
class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
<property name="authoritiesClaimName" value="authorities"/>
</bean>
You can also configure the authority prefix to be different as well.
Instead of prefixing each authority with SCOPE_
, you can change it to ROLE_
like so:
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_")
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
return jwtAuthenticationConverter
}
<http>
<intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
<intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
<oauth2-resource-server>
<jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
</oauth2-resource-server>
</http>
<bean id="jwtAuthenticationConverter"
class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
<property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>
<bean id="jwtGrantedAuthoritiesConverter"
class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
<property name="authorityPrefix" value="ROLE_"/>
</bean>
Or, you can remove the prefix altogether by calling JwtGrantedAuthoritiesConverter#setAuthorityPrefix("")
.
For more flexibility, the DSL supports entirely replacing the converter with any class that implements Converter<Jwt, AbstractAuthenticationToken>
:
static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
public AbstractAuthenticationToken convert(Jwt jwt) {
return new CustomAuthenticationToken(jwt);
}
}
// ...
@EnableWebSecurity
public class CustomAuthenticationConverterConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(new CustomAuthenticationConverter())
)
);
}
}
internal class CustomAuthenticationConverter : Converter<Jwt, AbstractAuthenticationToken> {
override fun convert(jwt: Jwt): AbstractAuthenticationToken {
return CustomAuthenticationToken(jwt)
}
}
// ...
@EnableWebSecurity
class CustomAuthenticationConverterConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = CustomAuthenticationConverter()
}
}
}
}
}
12.3.10. Configuring Validation
Using minimal Spring Boot configuration, indicating the authorization server’s issuer uri, Resource Server will default to verifying the iss
claim as well as the exp
and nbf
timestamp claims.
In circumstances where validation needs to be customized, Resource Server ships with two standard validators and also accepts custom OAuth2TokenValidator
instances.
Customizing Timestamp Validation
JWT’s typically have a window of validity, with the start of the window indicated in the nbf
claim and the end indicated in the exp
claim.
However, every server can experience clock drift, which can cause tokens to appear expired to one server, but not to another. This can cause some implementation heartburn as the number of collaborating servers increases in a distributed system.
Resource Server uses JwtTimestampValidator
to verify a token’s validity window, and it can be configured with a clockSkew
to alleviate the above problem:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.ofSeconds(60)),
new JwtIssuerValidator(issuerUri));
jwtDecoder.setJwtValidator(withClockSkew);
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder
val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(
JwtTimestampValidator(Duration.ofSeconds(60)),
JwtIssuerValidator(issuerUri))
jwtDecoder.setJwtValidator(withClockSkew)
return jwtDecoder
}
By default, Resource Server configures a clock skew of 60 seconds. |
Configuring a Custom Validator
Adding a check for the aud
claim is simple with the OAuth2TokenValidator
API:
OAuth2TokenValidator<Jwt> audienceValidator() {
return new JwtClaimValidator<List<String>>(AUD, aud -> aud.contains("messaging"));
}
fun audienceValidator(): OAuth2TokenValidator<Jwt?> {
return JwtClaimValidator<List<String>>(AUD) { aud -> aud.contains("messaging") }
}
Or, for more control you can implement your own OAuth2TokenValidator
:
static class AudienceValidator implements OAuth2TokenValidator<Jwt> {
OAuth2Error error = new OAuth2Error("custom_code", "Custom error message", null);
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (jwt.getAudience().contains("messaging")) {
return OAuth2TokenValidatorResult.success();
} else {
return OAuth2TokenValidatorResult.failure(error);
}
}
}
// ...
OAuth2TokenValidator<Jwt> audienceValidator() {
return new AudienceValidator();
}
internal class AudienceValidator : OAuth2TokenValidator<Jwt> {
var error: OAuth2Error = OAuth2Error("custom_code", "Custom error message", null)
override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
return if (jwt.audience.contains("messaging")) {
OAuth2TokenValidatorResult.success()
} else {
OAuth2TokenValidatorResult.failure(error)
}
}
}
// ...
fun audienceValidator(): OAuth2TokenValidator<Jwt> {
return AudienceValidator()
}
Then, to add into a resource server, it’s a matter of specifying the JwtDecoder
instance:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator();
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder
val audienceValidator = audienceValidator()
val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri)
val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)
jwtDecoder.setJwtValidator(withAudience)
return jwtDecoder
}
12.3.11. Configuring Claim Set Mapping
Spring Security uses the Nimbus library for parsing JWTs and validating their signatures. Consequently, Spring Security is subject to Nimbus’s interpretation of each field value and how to coerce each into a Java type.
For example, because Nimbus remains Java 7 compatible, it doesn’t use Instant
to represent timestamp fields.
And it’s entirely possible to use a different library or for JWT processing, which may make its own coercion decisions that need adjustment.
Or, quite simply, a resource server may want to add or remove claims from a JWT for domain-specific reasons.
For these purposes, Resource Server supports mapping the JWT claim set with MappedJwtClaimSetConverter
.
Customizing the Conversion of a Single Claim
By default, MappedJwtClaimSetConverter
will attempt to coerce claims into the following types:
Claim |
Java Type |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
An individual claim’s conversion strategy can be configured using MappedJwtClaimSetConverter.withDefaults
:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
.withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
jwtDecoder.setClaimSetConverter(converter);
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
val converter = MappedJwtClaimSetConverter
.withDefaults(mapOf("sub" to this::lookupUserIdBySub))
jwtDecoder.setClaimSetConverter(converter)
return jwtDecoder
}
This will keep all the defaults, except it will override the default claim converter for sub
.
Adding a Claim
MappedJwtClaimSetConverter
can also be used to add a custom claim, for example, to adapt to an existing system:
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter<Any, String> { "value" }))
Removing a Claim
And removing a claim is also simple, using the same API:
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter<Any, Any> { null }))
Renaming a Claim
In more sophisticated scenarios, like consulting multiple claims at once or renaming a claim, Resource Server accepts any class that implements Converter<Map<String, Object>, Map<String,Object>>
:
public class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
private final MappedJwtClaimSetConverter delegate =
MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
public Map<String, Object> convert(Map<String, Object> claims) {
Map<String, Object> convertedClaims = this.delegate.convert(claims);
String username = (String) convertedClaims.get("user_name");
convertedClaims.put("sub", username);
return convertedClaims;
}
}
class UsernameSubClaimAdapter : Converter<Map<String, Any?>, Map<String, Any?>> {
private val delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap())
override fun convert(claims: Map<String, Any?>): Map<String, Any?> {
val convertedClaims = delegate.convert(claims)
val username = convertedClaims["user_name"] as String
convertedClaims["sub"] = username
return convertedClaims
}
}
And then, the instance can be supplied like normal:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter())
return jwtDecoder
}
12.3.12. Configuring Timeouts
By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server.
This may be too short in some scenarios. Further, it doesn’t take into account more sophisticated patterns like back-off and discovery.
To adjust the way in which Resource Server connects to the authorization server, NimbusJwtDecoder
accepts an instance of RestOperations
:
@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
RestOperations rest = builder
.setConnectTimeout(Duration.ofSeconds(60))
.setReadTimeout(Duration.ofSeconds(60))
.build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build();
return jwtDecoder;
}
@Bean
fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder {
val rest: RestOperations = builder
.setConnectTimeout(Duration.ofSeconds(60))
.setReadTimeout(Duration.ofSeconds(60))
.build()
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build()
}
Also by default, Resource Server caches in-memory the authorization server’s JWK set for 5 minutes, which you may want to adjust. Further, it doesn’t take into account more sophisticated caching patterns like eviction or using a shared cache.
To adjust the way in which Resource Server caches the JWK set, NimbusJwtDecoder
accepts an instance of Cache
:
@Bean
public JwtDecoder jwtDecoder(CacheManager cacheManager) {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.cache(cacheManager.getCache("jwks"))
.build();
}
@Bean
fun jwtDecoder(cacheManager: CacheManager): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.cache(cacheManager.getCache("jwks"))
.build()
}
When given a Cache
, Resource Server will use the JWK Set Uri as the key and the JWK Set JSON as the value.
Spring isn’t a cache provider, so you’ll need to make sure to include the appropriate dependencies, like spring-boot-starter-cache and your favorite caching provider.
|
Whether it’s socket or cache timeouts, you may instead want to work with Nimbus directly.
To do so, remember that NimbusJwtDecoder ships with a constructor that takes Nimbus’s JWTProcessor .
|
12.3.13. Minimal Dependencies for Introspection
As described in Minimal Dependencies for JWT most of Resource Server support is collected in spring-security-oauth2-resource-server
.
However unless a custom OpaqueTokenIntrospector
is provided, the Resource Server will fallback to NimbusOpaqueTokenIntrospector.
Meaning that both spring-security-oauth2-resource-server
and oauth2-oidc-sdk
are necessary in order to have a working minimal Resource Server that supports opaque Bearer Tokens.
Please refer to spring-security-oauth2-resource-server
in order to determin the correct version for oauth2-oidc-sdk
.
12.3.14. Minimal Configuration for Introspection
Typically, an opaque token can be verified via an OAuth 2.0 Introspection Endpoint, hosted by the authorization server. This can be handy when revocation is a requirement.
When using Spring Boot, configuring an application as a resource server that uses introspection consists of two basic steps. First, include the needed dependencies and second, indicate the introspection endpoint details.
Specifying the Authorization Server
To specify where the introspection endpoint is, simply do:
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.com/introspect
client-id: client
client-secret: secret
Where https://idp.example.com/introspect
is the introspection endpoint hosted by your authorization server and client-id
and client-secret
are the credentials needed to hit that endpoint.
Resource Server will use these properties to further self-configure and subsequently validate incoming JWTs.
When using introspection, the authorization server’s word is the law. If the authorization server responses that the token is valid, then it is. |
And that’s it!
Startup Expectations
When this property and these dependencies are used, Resource Server will automatically configure itself to validate Opaque Bearer Tokens.
This startup process is quite a bit simpler than for JWTs since no endpoints need to be discovered and no additional validation rules get added.
Runtime Expectations
Once the application is started up, Resource Server will attempt to process any request containing an Authorization: Bearer
header:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
Given an Opaque Token, Resource Server will
-
Query the provided introspection endpoint using the provided credentials and the token
-
Inspect the response for an
{ 'active' : true }
attribute -
Map each scope to an authority with the prefix
SCOPE_
The resulting Authentication#getPrincipal
, by default, is a Spring Security OAuth2AuthenticatedPrincipal
object, and Authentication#getName
maps to the token’s sub
property, if one is present.
From here, you may want to jump to:
12.3.15. How Opaque Token Authentication Works
Next, let’s see the architectural components that Spring Security uses to support opaque token Authentication in servlet-based applications, like the one we just saw.
OpaqueTokenAuthenticationProvider
is an AuthenticationProvider
implementation that leverages a OpaqueTokenIntrospector
to authenticate an opaque token.
Let’s take a look at how OpaqueTokenAuthenticationProvider
works within Spring Security.
The figure explains details of how the AuthenticationManager
in figures from Reading the Bearer Token works.
OpaqueTokenAuthenticationProvider
Usage The authentication Filter
from Reading the Bearer Token passes a BearerTokenAuthenticationToken
to the AuthenticationManager
which is implemented by ProviderManager
.
The ProviderManager
is configured to use an AuthenticationProvider of type OpaqueTokenAuthenticationProvider
.
OpaqueTokenAuthenticationProvider
introspects the opaque token and adds granted authorities using an OpaqueTokenIntrospector
.
When authentication is successful, the Authentication
that is returned is of type BearerTokenAuthentication
and has a principal that is the OAuth2AuthenticatedPrincipal
returned by the configured OpaqueTokenIntrospector
.
Ultimately, the returned BearerTokenAuthentication
will be set on the SecurityContextHolder
by the authentication Filter
.
12.3.16. Looking Up Attributes Post-Authentication
Once a token is authenticated, an instance of BearerTokenAuthentication
is set in the SecurityContext
.
This means that it’s available in @Controller
methods when using @EnableWebMvc
in your configuration:
@GetMapping("/foo")
public String foo(BearerTokenAuthentication authentication) {
return authentication.getTokenAttributes().get("sub") + " is the subject";
}
@GetMapping("/foo")
fun foo(authentication: BearerTokenAuthentication): String {
return authentication.tokenAttributes["sub"].toString() + " is the subject"
}
Since BearerTokenAuthentication
holds an OAuth2AuthenticatedPrincipal
, that also means that it’s available to controller methods, too:
@GetMapping("/foo")
public String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
return principal.getAttribute("sub") + " is the subject";
}
@GetMapping("/foo")
fun foo(@AuthenticationPrincipal principal: OAuth2AuthenticatedPrincipal): String {
return principal.getAttribute<Any>("sub").toString() + " is the subject"
}
Looking Up Attributes Via SpEL
Of course, this also means that attributes can be accessed via SpEL.
For example, if using @EnableGlobalMethodSecurity
so that you can use @PreAuthorize
annotations, you can do:
@PreAuthorize("principal?.attributes['sub'] == 'foo'")
public String forFoosEyesOnly() {
return "foo";
}
@PreAuthorize("principal?.attributes['sub'] == 'foo'")
fun forFoosEyesOnly(): String {
return "foo"
}
12.3.17. Overriding or Replacing Boot Auto Configuration
There are two @Bean
s that Spring Boot generates on Resource Server’s behalf.
The first is a WebSecurityConfigurerAdapter
that configures the app as a resource server.
When use Opaque Token, this WebSecurityConfigurerAdapter
looks like:
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
}
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken { }
}
}
}
If the application doesn’t expose a WebSecurityConfigurerAdapter
bean, then Spring Boot will expose the above default one.
Replacing this is as simple as exposing the bean within the application:
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaqueToken -> opaqueToken
.introspector(myIntrospector())
)
);
}
}
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("SCOPE_message:read"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken {
introspector = myIntrospector()
}
}
}
}
}
The above requires the scope of message:read
for any URL that starts with /messages/
.
Methods on the oauth2ResourceServer
DSL will also override or replace auto configuration.
For example, the second @Bean
Spring Boot creates is an OpaqueTokenIntrospector
, which decodes String
tokens into validated instances of OAuth2AuthenticatedPrincipal
:
@Bean
public OpaqueTokenIntrospector introspector() {
return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret)
}
If the application doesn’t expose a OpaqueTokenIntrospector
bean, then Spring Boot will expose the above default one.
And its configuration can be overridden using introspectionUri()
and introspectionClientCredentials()
or replaced using introspector()
.
Or, if you’re not using Spring Boot at all, then both of these components - the filter chain and a OpaqueTokenIntrospector
can be specified in XML.
The filter chain is specified like so:
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<opaque-token introspector-ref="opaqueTokenIntrospector"/>
</oauth2-resource-server>
</http>
And the OpaqueTokenIntrospector
like so:
<bean id="opaqueTokenIntrospector"
class="org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector">
<constructor-arg value="${spring.security.oauth2.resourceserver.opaquetoken.introspection_uri}"/>
<constructor-arg value="${spring.security.oauth2.resourceserver.opaquetoken.client_id}"/>
<constructor-arg value="${spring.security.oauth2.resourceserver.opaquetoken.client_secret}"/>
</bean>
Using introspectionUri()
An authorization server’s Introspection Uri can be configured as a configuration property or it can be supplied in the DSL:
@EnableWebSecurity
public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaqueToken -> opaqueToken
.introspectionUri("https://idp.example.com/introspect")
.introspectionClientCredentials("client", "secret")
)
);
}
}
@EnableWebSecurity
class DirectlyConfiguredIntrospectionUri : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken {
introspectionUri = "https://idp.example.com/introspect"
introspectionClientCredentials("client", "secret")
}
}
}
}
}
<bean id="opaqueTokenIntrospector"
class="org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector">
<constructor-arg value="https://idp.example.com/introspect"/>
<constructor-arg value="client"/>
<constructor-arg value="secret"/>
</bean>
Using introspectionUri()
takes precedence over any configuration property.
Using introspector()
More powerful than introspectionUri()
is introspector()
, which will completely replace any Boot auto configuration of OpaqueTokenIntrospector
:
@EnableWebSecurity
public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaqueToken -> opaqueToken
.introspector(myCustomIntrospector())
)
);
}
}
@EnableWebSecurity
class DirectlyConfiguredIntrospector : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken {
introspector = myCustomIntrospector()
}
}
}
}
}
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<opaque-token introspector-ref="myCustomIntrospector"/>
</oauth2-resource-server>
</http>
This is handy when deeper configuration, like authority mapping, JWT revocation, or request timeouts, is necessary.
Exposing a OpaqueTokenIntrospector
@Bean
Or, exposing a OpaqueTokenIntrospector
@Bean
has the same effect as introspector()
:
@Bean
public OpaqueTokenIntrospector introspector() {
return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}
12.3.18. Configuring Authorization
An OAuth 2.0 Introspection endpoint will typically return a scope
attribute, indicating the scopes (or authorities) it’s been granted, for example:
{ …, "scope" : "messages contacts"}
When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".
This means that to protect an endpoint or method with a scope derived from an Opaque Token, the corresponding expressions should include this prefix:
@EnableWebSecurity
public class MappedAuthorities extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
}
}
@EnableWebSecurity
class MappedAuthorities : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
authorize("/messages/**", hasAuthority("SCOPE_messages"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken { }
}
}
}
}
<http>
<intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
<intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
<oauth2-resource-server>
<opaque-token introspector-ref="opaqueTokenIntrospector"/>
</oauth2-resource-server>
</http>
Or similarly with method security:
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): List<Message?> {}
Extracting Authorities Manually
By default, Opaque Token support will extract the scope claim from an introspection response and parse it into individual GrantedAuthority
instances.
For example, if the introspection response were:
{
"active" : true,
"scope" : "message:read message:write"
}
Then Resource Server would generate an Authentication
with two authorities, one for message:read
and the other for message:write
.
This can, of course, be customized using a custom OpaqueTokenIntrospector
that takes a look at the attribute set and converts in its own way:
public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
return new DefaultOAuth2AuthenticatedPrincipal(
principal.getName(), principal.getAttributes(), extractAuthorities(principal));
}
private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
return scopes.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
class CustomAuthoritiesOpaqueTokenIntrospector : OpaqueTokenIntrospector {
private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
override fun introspect(token: String): OAuth2AuthenticatedPrincipal {
val principal: OAuth2AuthenticatedPrincipal = delegate.introspect(token)
return DefaultOAuth2AuthenticatedPrincipal(
principal.name, principal.attributes, extractAuthorities(principal))
}
private fun extractAuthorities(principal: OAuth2AuthenticatedPrincipal): Collection<GrantedAuthority> {
val scopes: List<String> = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE)
return scopes
.map { SimpleGrantedAuthority(it) }
}
}
Thereafter, this custom introspector can be configured simply by exposing it as a @Bean
:
@Bean
public OpaqueTokenIntrospector introspector() {
return new CustomAuthoritiesOpaqueTokenIntrospector();
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return CustomAuthoritiesOpaqueTokenIntrospector()
}
12.3.19. Configuring Timeouts
By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server.
This may be too short in some scenarios. Further, it doesn’t take into account more sophisticated patterns like back-off and discovery.
To adjust the way in which Resource Server connects to the authorization server, NimbusOpaqueTokenIntrospector
accepts an instance of RestOperations
:
@Bean
public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder, OAuth2ResourceServerProperties properties) {
RestOperations rest = builder
.basicAuthentication(properties.getOpaquetoken().getClientId(), properties.getOpaquetoken().getClientSecret())
.setConnectTimeout(Duration.ofSeconds(60))
.setReadTimeout(Duration.ofSeconds(60))
.build();
return new NimbusOpaqueTokenIntrospector(introspectionUri, rest);
}
@Bean
fun introspector(builder: RestTemplateBuilder, properties: OAuth2ResourceServerProperties): OpaqueTokenIntrospector? {
val rest: RestOperations = builder
.basicAuthentication(properties.opaquetoken.clientId, properties.opaquetoken.clientSecret)
.setConnectTimeout(Duration.ofSeconds(60))
.setReadTimeout(Duration.ofSeconds(60))
.build()
return NimbusOpaqueTokenIntrospector(introspectionUri, rest)
}
12.3.20. Using Introspection with JWTs
A common question is whether or not introspection is compatible with JWTs. Spring Security’s Opaque Token support has been designed to not care about the format of the token — it will gladly pass any token to the introspection endpoint provided.
So, let’s say that you’ve got a requirement that requires you to check with the authorization server on each request, in case the JWT has been revoked.
Even though you are using the JWT format for the token, your validation method is introspection, meaning you’d want to do:
spring:
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.org/introspection
client-id: client
client-secret: secret
In this case, the resulting Authentication
would be BearerTokenAuthentication
.
Any attributes in the corresponding OAuth2AuthenticatedPrincipal
would be whatever was returned by the introspection endpoint.
But, let’s say that, oddly enough, the introspection endpoint only returns whether or not the token is active. Now what?
In this case, you can create a custom OpaqueTokenIntrospector
that still hits the endpoint, but then updates the returned principal to have the JWTs claims as the attributes:
public class JwtOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private JwtDecoder jwtDecoder = new NimbusJwtDecoder(new ParseOnlyJWTProcessor());
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
try {
Jwt jwt = this.jwtDecoder.decode(token);
return new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES);
} catch (JwtException ex) {
throw new OAuth2IntrospectionException(ex);
}
}
private static class ParseOnlyJWTProcessor extends DefaultJWTProcessor<SecurityContext> {
JWTClaimsSet process(SignedJWT jwt, SecurityContext context)
throws JOSEException {
return jwt.getJWTClaimsSet();
}
}
}
class JwtOpaqueTokenIntrospector : OpaqueTokenIntrospector {
private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
private val jwtDecoder: JwtDecoder = NimbusJwtDecoder(ParseOnlyJWTProcessor())
override fun introspect(token: String): OAuth2AuthenticatedPrincipal {
val principal = delegate.introspect(token)
return try {
val jwt: Jwt = jwtDecoder.decode(token)
DefaultOAuth2AuthenticatedPrincipal(jwt.claims, NO_AUTHORITIES)
} catch (ex: JwtException) {
throw OAuth2IntrospectionException(ex.message)
}
}
private class ParseOnlyJWTProcessor : DefaultJWTProcessor<SecurityContext>() {
override fun process(jwt: SignedJWT, context: SecurityContext): JWTClaimsSet {
return jwt.jwtClaimsSet
}
}
}
Thereafter, this custom introspector can be configured simply by exposing it as a @Bean
:
@Bean
public OpaqueTokenIntrospector introspector() {
return new JwtOpaqueTokenIntrospector();
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return JwtOpaqueTokenIntrospector()
}
12.3.21. Calling a /userinfo
Endpoint
Generally speaking, a Resource Server doesn’t care about the underlying user, but instead about the authorities that have been granted.
That said, at times it can be valuable to tie the authorization statement back to a user.
If an application is also using spring-security-oauth2-client
, having set up the appropriate ClientRegistrationRepository
, then this is quite simple with a custom OpaqueTokenIntrospector
.
This implementation below does three things:
-
Delegates to the introspection endpoint, to affirm the token’s validity
-
Looks up the appropriate client registration associated with the
/userinfo
endpoint -
Invokes and returns the response from the
/userinfo
endpoint
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private final OAuth2UserService oauth2UserService = new DefaultOAuth2UserService();
private final ClientRegistrationRepository repository;
// ... constructor
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
Instant issuedAt = authorized.getAttribute(ISSUED_AT);
Instant expiresAt = authorized.getAttribute(EXPIRES_AT);
ClientRegistration clientRegistration = this.repository.findByRegistrationId("registration-id");
OAuth2AccessToken token = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt);
OAuth2UserRequest oauth2UserRequest = new OAuth2UserRequest(clientRegistration, token);
return this.oauth2UserService.loadUser(oauth2UserRequest);
}
}
class UserInfoOpaqueTokenIntrospector : OpaqueTokenIntrospector {
private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
private val oauth2UserService = DefaultOAuth2UserService()
private val repository: ClientRegistrationRepository? = null
// ... constructor
override fun introspect(token: String): OAuth2AuthenticatedPrincipal {
val authorized = delegate.introspect(token)
val issuedAt: Instant? = authorized.getAttribute(ISSUED_AT)
val expiresAt: Instant? = authorized.getAttribute(EXPIRES_AT)
val clientRegistration: ClientRegistration = repository!!.findByRegistrationId("registration-id")
val accessToken = OAuth2AccessToken(BEARER, token, issuedAt, expiresAt)
val oauth2UserRequest = OAuth2UserRequest(clientRegistration, accessToken)
return oauth2UserService.loadUser(oauth2UserRequest)
}
}
If you aren’t using spring-security-oauth2-client
, it’s still quite simple.
You will simply need to invoke the /userinfo
with your own instance of WebClient
:
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private final WebClient rest = WebClient.create();
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
return makeUserInfoRequest(authorized);
}
}
class UserInfoOpaqueTokenIntrospector : OpaqueTokenIntrospector {
private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
private val rest: WebClient = WebClient.create()
override fun introspect(token: String): OAuth2AuthenticatedPrincipal {
val authorized = delegate.introspect(token)
return makeUserInfoRequest(authorized)
}
}
Either way, having created your OpaqueTokenIntrospector
, you should publish it as a @Bean
to override the defaults:
@Bean
OpaqueTokenIntrospector introspector() {
return new UserInfoOpaqueTokenIntrospector(...);
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return UserInfoOpaqueTokenIntrospector(...)
}
12.3.22. Supporting both JWT and Opaque Token
In some cases, you may have a need to access both kinds of tokens. For example, you may support more than one tenant where one tenant issues JWTs and the other issues opaque tokens.
If this decision must be made at request-time, then you can use an AuthenticationManagerResolver
to achieve it, like so:
@Bean
AuthenticationManagerResolver<HttpServletRequest> tokenAuthenticationManagerResolver() {
BearerTokenResolver bearerToken = new DefaultBearerTokenResolver();
JwtAuthenticationProvider jwt = jwt();
OpaqueTokenAuthenticationProvider opaqueToken = opaqueToken();
return request -> {
if (useJwt(request)) {
return jwt::authenticate;
} else {
return opaqueToken::authenticate;
}
}
}
@Bean
fun tokenAuthenticationManagerResolver(): AuthenticationManagerResolver<HttpServletRequest> {
val bearerToken: BearerTokenResolver = DefaultBearerTokenResolver()
val jwt: JwtAuthenticationProvider = jwt()
val opaqueToken: OpaqueTokenAuthenticationProvider = opaqueToken()
return AuthenticationManagerResolver { request ->
if (useJwt(request)) {
AuthenticationManager { jwt.authenticate(it) }
} else {
AuthenticationManager { opaqueToken.authenticate(it) }
}
}
}
The implementation of useJwt(HttpServletRequest) will likely depend on custom request material like the path.
|
And then specify this AuthenticationManagerResolver
in the DSL:
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(this.tokenAuthenticationManagerResolver)
);
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
authenticationManagerResolver = tokenAuthenticationManagerResolver()
}
}
<http>
<oauth2-resource-server authentication-manager-resolver-ref="tokenAuthenticationManagerResolver"/>
</http>
12.3.23. Multi-tenancy
A resource server is considered multi-tenant when there are multiple strategies for verifying a bearer token, keyed by some tenant identifier.
For example, your resource server may accept bearer tokens from two different authorization servers. Or, your authorization server may represent a multiplicity of issuers.
In each case, there are two things that need to be done and trade-offs associated with how you choose to do them:
-
Resolve the tenant
-
Propagate the tenant
Resolving the Tenant By Claim
One way to differentiate tenants is by the issuer claim. Since the issuer claim accompanies signed JWTs, this can be done with the JwtIssuerAuthenticationManagerResolver
, like so:
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver
("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(authenticationManagerResolver)
);
val customAuthenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo")
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
authenticationManagerResolver = customAuthenticationManagerResolver
}
}
<http>
<oauth2-resource-server authentication-manager-resolver-ref="authenticationManagerResolver"/>
</http>
<bean id="authenticationManagerResolver"
class="org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver">
<constructor-arg>
<list>
<value>https://idp.example.org/issuerOne</value>
<value>https://idp.example.org/issuerTwo</value>
</list>
</constructor-arg>
</bean>
This is nice because the issuer endpoints are loaded lazily.
In fact, the corresponding JwtAuthenticationProvider
is instantiated only when the first request with the corresponding issuer is sent.
This allows for an application startup that is independent from those authorization servers being up and available.
Dynamic Tenants
Of course, you may not want to restart the application each time a new tenant is added.
In this case, you can configure the JwtIssuerAuthenticationManagerResolver
with a repository of AuthenticationManager
instances, which you can edit at runtime, like so:
private void addManager(Map<String, AuthenticationManager> authenticationManagers, String issuer) {
JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider
(JwtDecoders.fromIssuerLocation(issuer));
authenticationManagers.put(issuer, authenticationProvider::authenticate);
}
// ...
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(authenticationManagerResolver)
);
private fun addManager(authenticationManagers: MutableMap<String, AuthenticationManager>, issuer: String) {
val authenticationProvider = JwtAuthenticationProvider(JwtDecoders.fromIssuerLocation(issuer))
authenticationManagers[issuer] = AuthenticationManager {
authentication: Authentication? -> authenticationProvider.authenticate(authentication)
}
}
// ...
val customAuthenticationManagerResolver: JwtIssuerAuthenticationManagerResolver =
JwtIssuerAuthenticationManagerResolver(authenticationManagers::get)
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
authenticationManagerResolver = customAuthenticationManagerResolver
}
}
In this case, you construct JwtIssuerAuthenticationManagerResolver
with a strategy for obtaining the AuthenticationManager
given the issuer.
This approach allows us to add and remove elements from the repository (shown as a Map
in the snippet) at runtime.
It would be unsafe to simply take any issuer and construct an AuthenticationManager from it.
The issuer should be one that the code can verify from a trusted source like a list of allowed issuers.
|
Parsing the Claim Only Once
You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the AuthenticationManagerResolver
and then again by the JwtDecoder
later on in the request.
This extra parsing can be alleviated by configuring the JwtDecoder
directly with a JWTClaimsSetAwareJWSKeySelector
from Nimbus:
@Component
public class TenantJWSKeySelector
implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
private final TenantRepository tenants; (1)
private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>(); (2)
public TenantJWSKeySelector(TenantRepository tenants) {
this.tenants = tenants;
}
@Override
public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext)
throws KeySourceException {
return this.selectors.computeIfAbsent(toTenant(jwtClaimsSet), this::fromTenant)
.selectJWSKeys(jwsHeader, securityContext);
}
private String toTenant(JWTClaimsSet claimSet) {
return (String) claimSet.getClaim("iss");
}
private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
return Optional.ofNullable(this.tenantRepository.findById(tenant)) (3)
.map(t -> t.getAttrbute("jwks_uri"))
.map(this::fromUri)
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
}
private JWSKeySelector<SecurityContext> fromUri(String uri) {
try {
return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri)); (4)
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
}
@Component
class TenantJWSKeySelector(tenants: TenantRepository) : JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
private val tenants: TenantRepository (1)
private val selectors: MutableMap<String, JWSKeySelector<SecurityContext>> = ConcurrentHashMap() (2)
init {
this.tenants = tenants
}
fun selectKeys(jwsHeader: JWSHeader?, jwtClaimsSet: JWTClaimsSet, securityContext: SecurityContext): List<Key?> {
return selectors.computeIfAbsent(toTenant(jwtClaimsSet)) { tenant: String -> fromTenant(tenant) }
.selectJWSKeys(jwsHeader, securityContext)
}
private fun toTenant(claimSet: JWTClaimsSet): String {
return claimSet.getClaim("iss") as String
}
private fun fromTenant(tenant: String): JWSKeySelector<SecurityContext> {
return Optional.ofNullable(this.tenants.findById(tenant)) (3)
.map { t -> t.getAttrbute("jwks_uri") }
.map { uri: String -> fromUri(uri) }
.orElseThrow { IllegalArgumentException("unknown tenant") }
}
private fun fromUri(uri: String): JWSKeySelector<SecurityContext?> {
return try {
JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(URL(uri)) (4)
} catch (ex: Exception) {
throw IllegalArgumentException(ex)
}
}
}
1 | A hypothetical source for tenant information |
2 | A cache for `JWKKeySelector`s, keyed by tenant identifier |
3 | Looking up the tenant is more secure than simply calculating the JWK Set endpoint on the fly - the lookup acts as a list of allowed tenants |
4 | Create a JWSKeySelector via the types of keys that come back from the JWK Set endpoint - the lazy lookup here means that you don’t need to configure all tenants at startup |
The above key selector is a composition of many key selectors.
It chooses which key selector to use based on the iss
claim in the JWT.
To use this approach, make sure that the authorization server is configured to include the claim set as part of the token’s signature. Without this, you have no guarantee that the issuer hasn’t been altered by a bad actor. |
Next, we can construct a JWTProcessor
:
@Bean
JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor();
jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector);
return jwtProcessor;
}
@Bean
fun jwtProcessor(keySelector: JWTClaimsSetAwareJWSKeySelector<SecurityContext>): JWTProcessor<SecurityContext> {
val jwtProcessor = DefaultJWTProcessor<SecurityContext>()
jwtProcessor.jwtClaimsSetAwareJWSKeySelector = keySelector
return jwtProcessor
}
As you are already seeing, the trade-off for moving tenant-awareness down to this level is more configuration. We have just a bit more.
Next, we still want to make sure you are validating the issuer. But, since the issuer may be different per JWT, then you’ll need a tenant-aware validator, too:
@Component
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
private final TenantRepository tenants;
private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
public TenantJwtIssuerValidator(TenantRepository tenants) {
this.tenants = tenants;
}
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
return this.validators.computeIfAbsent(toTenant(token), this::fromTenant)
.validate(token);
}
private String toTenant(Jwt jwt) {
return jwt.getIssuer();
}
private JwtIssuerValidator fromTenant(String tenant) {
return Optional.ofNullable(this.tenants.findById(tenant))
.map(t -> t.getAttribute("issuer"))
.map(JwtIssuerValidator::new)
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
}
}
@Component
class TenantJwtIssuerValidator(tenants: TenantRepository) : OAuth2TokenValidator<Jwt> {
private val tenants: TenantRepository
private val validators: MutableMap<String, JwtIssuerValidator> = ConcurrentHashMap()
override fun validate(token: Jwt): OAuth2TokenValidatorResult {
return validators.computeIfAbsent(toTenant(token)) { tenant: String -> fromTenant(tenant) }
.validate(token)
}
private fun toTenant(jwt: Jwt): String {
return jwt.issuer.toString()
}
private fun fromTenant(tenant: String): JwtIssuerValidator {
return Optional.ofNullable(tenants.findById(tenant))
.map({ t -> t.getAttribute("issuer") })
.map({ JwtIssuerValidator() })
.orElseThrow({ IllegalArgumentException("unknown tenant") })
}
init {
this.tenants = tenants
}
}
Now that we have a tenant-aware processor and a tenant-aware validator, we can proceed with creating our JwtDecoder
:
@Bean
JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
(JwtValidators.createDefault(), this.jwtValidator);
decoder.setJwtValidator(validator);
return decoder;
}
@Bean
fun jwtDecoder(jwtProcessor: JWTProcessor<SecurityContext>?, jwtValidator: OAuth2TokenValidator<Jwt>?): JwtDecoder {
val decoder = NimbusJwtDecoder(jwtProcessor)
val validator: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(JwtValidators.createDefault(), jwtValidator)
decoder.setJwtValidator(validator)
return decoder
}
We’ve finished talking about resolving the tenant.
If you’ve chosen to resolve the tenant by something other than a JWT claim, then you’ll need to make sure you address your downstream resource servers in the same way. For example, if you are resolving it by subdomain, you may need to address the downstream resource server using the same subdomain.
However, if you resolve it by a claim in the bearer token, read on to learn about Spring Security’s support for bearer token propagation.
12.3.24. Bearer Token Resolution
By default, Resource Server looks for a bearer token in the Authorization
header.
This, however, can be customized in a handful of ways.
Reading the Bearer Token from a Custom Header
For example, you may have a need to read the bearer token from a custom header.
To achieve this, you can expose a DefaultBearerTokenResolver
as a bean, or wire an instance into the DSL, as you can see in the following example:
@Bean
BearerTokenResolver bearerTokenResolver() {
DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION);
return bearerTokenResolver;
}
@Bean
fun bearerTokenResolver(): BearerTokenResolver {
val bearerTokenResolver = DefaultBearerTokenResolver()
bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION)
return bearerTokenResolver
}
<http>
<oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver"/>
</http>
<bean id="bearerTokenResolver"
class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver">
<property name="bearerTokenHeaderName" value="Proxy-Authorization"/>
</bean>
Or, in circumstances where a provider is using both a custom header and value, you can use HeaderBearerTokenResolver
instead.
Reading the Bearer Token from a Form Parameter
Or, you may wish to read the token from a form parameter, which you can do by configuring the DefaultBearerTokenResolver
, as you can see below:
DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
resolver.setAllowFormEncodedBodyParameter(true);
http
.oauth2ResourceServer(oauth2 -> oauth2
.bearerTokenResolver(resolver)
);
val resolver = DefaultBearerTokenResolver()
resolver.setAllowFormEncodedBodyParameter(true)
http {
oauth2ResourceServer {
bearerTokenResolver = resolver
}
}
<http>
<oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver"/>
</http>
<bean id="bearerTokenResolver"
class="org.springframework.security.oauth2.server.resource.web.HeaderBearerTokenResolver">
<property name="allowFormEncodedBodyParameter" value="true"/>
</bean>
12.3.25. Bearer Token Propagation
Now that you’re resource server has validated the token, it might be handy to pass it to downstream services.
This is quite simple with ServletBearerExchangeFilterFunction
, which you can see in the following example:
@Bean
public WebClient rest() {
return WebClient.builder()
.filter(new ServletBearerExchangeFilterFunction())
.build();
}
@Bean
fun rest(): WebClient {
return WebClient.builder()
.filter(ServletBearerExchangeFilterFunction())
.build()
}
When the above WebClient
is used to perform requests, Spring Security will look up the current Authentication
and extract any AbstractOAuth2Token
credential.
Then, it will propagate that token in the Authorization
header.
For example:
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.retrieve()
.bodyToMono(String.class)
.block()
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.retrieve()
.bodyToMono<String>()
.block()
Will invoke the https://other-service.example.com/endpoint
, adding the bearer token Authorization
header for you.
In places where you need to override this behavior, it’s a simple matter of supplying the header yourself, like so:
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.headers(headers -> headers.setBearerAuth(overridingToken))
.retrieve()
.bodyToMono(String.class)
.block()
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.headers{ headers -> headers.setBearerAuth(overridingToken)}
.retrieve()
.bodyToMono<String>()
.block()
In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain.
Unlike the OAuth 2.0 Client filter function, this filter function makes no attempt to renew the token, should it be expired. To obtain this level of support, please use the OAuth 2.0 Client filter. |
RestTemplate
support
There is no RestTemplate
equivalent for ServletBearerExchangeFilterFunction
at the moment, but you can propagate the request’s bearer token quite simply with your own interceptor:
@Bean
RestTemplate rest() {
RestTemplate rest = new RestTemplate();
rest.getInterceptors().add((request, body, execution) -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return execution.execute(request, body);
}
if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) {
return execution.execute(request, body);
}
AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials();
request.getHeaders().setBearerAuth(token.getTokenValue());
return execution.execute(request, body);
});
return rest;
}
@Bean
fun rest(): RestTemplate {
val rest = RestTemplate()
rest.interceptors.add(ClientHttpRequestInterceptor { request, body, execution ->
val authentication: Authentication? = SecurityContextHolder.getContext().authentication
if (authentication != null) {
execution.execute(request, body)
}
if (authentication!!.credentials !is AbstractOAuth2Token) {
execution.execute(request, body)
}
val token: AbstractOAuth2Token = authentication.credentials as AbstractOAuth2Token
request.headers.setBearerAuth(token.tokenValue)
execution.execute(request, body)
})
return rest
}
Unlike the OAuth 2.0 Authorized Client Manager, this filter interceptor makes no attempt to renew the token, should it be expired. To obtain this level of support, please create an interceptor using the OAuth 2.0 Authorized Client Manager. |
12.3.26. Bearer Token Failure
A bearer token may be invalid for a number of reasons. For example, the token may no longer be active.
In these circumstances, Resource Server throws an InvalidBearerTokenException
.
Like other exceptions, this results in an OAuth 2.0 Bearer Token error response:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error_code="invalid_token", error_description="Unsupported algorithm of none", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
Additionally, it is published as an AuthenticationFailureBadCredentialsEvent
, which you can listen for in your application like so:
@Component
public class FailureEvents {
@EventListener
public void onFailure(AuthenticationFailureBadCredentialsEvent badCredentials) {
if (badCredentials.getAuthentication() instanceof BearerTokenAuthenticationToken) {
// ... handle
}
}
}
@Component
class FailureEvents {
@EventListener
fun onFailure(badCredentials: AuthenticationFailureBadCredentialsEvent) {
if (badCredentials.authentication is BearerTokenAuthenticationToken) {
// ... handle
}
}
}
13. SAML2
13.1. SAML 2.0 Login
The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Relying Party, having users log in to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc).
SAML 2.0 Login is implemented by using the Web Browser SSO Profile, as specified in SAML 2 Profiles. |
Since 2009, support for relying parties has existed as an extension project. In 2019, the process began to port that into Spring Security proper. This process is similar to the one started in 2017 for Spring Security’s OAuth 2.0 support.
A working sample for SAML 2.0 Login is available in the Spring Security Samples repository. |
Let’s take a look at how SAML 2.0 Relying Party Authentication works within Spring Security. First, we see that, like OAuth 2.0 Login, Spring Security takes the user to a third-party for performing authentication. It does this through a series of redirects.
The figure above builds off our SecurityFilterChain
and AbstractAuthenticationProcessingFilter
diagrams:
First, a user makes an unauthenticated request to the resource /private
for which it is not authorized.
Spring Security’s FilterSecurityInterceptor
indicates that the unauthenticated request is Denied by throwing an AccessDeniedException
.
Since the user lacks authorization, the ExceptionTranslationFilter
initiates Start Authentication.
The configured AuthenticationEntryPoint
is an instance of LoginUrlAuthenticationEntryPoint
which redirects to the <saml2:AuthnRequest>
generating endpoint, Saml2WebSsoAuthenticationRequestFilter
.
Or, if you’ve configured more than one asserting party, it will first redirect to a picker page.
Next, the Saml2WebSsoAuthenticationRequestFilter
creates, signs, serializes, and encodes a <saml2:AuthnRequest>
using its configured Saml2AuthenticationRequestFactory
.
Then, the browser takes this <saml2:AuthnRequest>
and presents it to the asserting party.
The asserting party attempts to authentication the user.
If successful, it will return a <saml2:Response>
back to the browser.
The browser then POSTs the <saml2:Response>
to the assertion consumer service endpoint.
<saml2:Response>
The figure builds off our SecurityFilterChain
diagram.
When the browser submits a <saml2:Response>
to the application, it delegates to Saml2WebSsoAuthenticationFilter
.
This filter calls its configured AuthenticationConverter
to create a Saml2AuthenticationToken
by extracting the response from the HttpServletRequest
.
This converter additionally resolves the RelyingPartyRegistration
and supplies it to Saml2AuthenticationToken
.
Next, the filter passes the token to its configured AuthenticationManager
.
By default, it will use the OpenSAML authentication provider
.
If authentication fails, then Failure
-
The
SecurityContextHolder
is cleared out. -
The
AuthenticationEntryPoint
is invoked to restart the authentication process.
If authentication is successful, then Success.
-
The
Authentication
is set on theSecurityContextHolder
. -
The
Saml2WebSsoAuthenticationFilter
invokesFilterChain#doFilter(request,response)
to continue with the rest of the application logic.
13.1.1. Minimal Dependencies
SAML 2.0 service provider support resides in spring-security-saml2-service-provider
.
It builds off of the OpenSAML library.
13.1.2. Minimal Configuration
When using Spring Boot, configuring an application as a service provider consists of two basic steps. First, include the needed dependencies and second, indicate the necessary asserting party metadata.
Also, this presupposes that you’ve already registered the relying party with your asserting party. |
Specifying Identity Provider Metadata
In a Spring Boot application, to specify an identity provider’s metadata, simply do:
spring:
security:
saml2:
relyingparty:
registration:
adfs:
identityprovider:
entity-id: https://idp.example.com/issuer
verification.credentials:
- certificate-location: "classpath:idp.crt"
singlesignon.url: https://idp.example.com/issuer/sso
singlesignon.sign-request: false
where
-
https://idp.example.com/issuer
is the value contained in theIssuer
attribute of the SAML responses that the identity provider will issue -
classpath:idp.crt
is the location on the classpath for the identity provider’s certificate for verifying SAML responses, and -
https://idp.example.com/issuer/sso
is the endpoint where the identity provider is expectingAuthnRequest
s.
And that’s it!
Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party. These are frequently abbreviated as AP and RP, respectively. |
Runtime Expectations
As configured above, the application processes any POST /login/saml2/sso/{registrationId}
request containing a SAMLResponse
parameter:
POST /login/saml2/sso/adfs HTTP/1.1
SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
There are two ways to see induce your asserting party to generate a SAMLResponse
:
-
First, you can navigate to your asserting party. It likely has some kind of link or button for each registered relying party that you can click to send the
SAMLResponse
. -
Second, you can navigate to a protected page in your app, for example,
http://localhost:8080
. Your app then redirects to the configured asserting party which then sends theSAMLResponse
.
From here, consider jumping to:
13.1.3. How SAML 2.0 Login Integrates with OpenSAML
Spring Security’s SAML 2.0 support has a couple of design goals:
-
First, rely on a library for SAML 2.0 operations and domain objects. To achieve this, Spring Security uses OpenSAML.
-
Second, ensure this library is not required when using Spring Security’s SAML support. To achieve this, any interfaces or classes where Spring Security uses OpenSAML in the contract remain encapsulated. This makes it possible for you to switch out OpenSAML for some other library or even an unsupported version of OpenSAML.
As a natural outcome of the above two goals, Spring Security’s SAML API is quite small relative to other modules.
Instead, classes like OpenSaml4AuthenticationRequestFactory
and OpenSaml4AuthenticationProvider
expose Converter
s that customize various steps in the authentication process.
For example, once your application receives a SAMLResponse
and delegates to Saml2WebSsoAuthenticationFilter
, the filter will delegate to OpenSaml4AuthenticationProvider
.
For backward compatibility, Spring Security will use the latest OpenSAML 3 by default. Note, though that OpenSAML 3 has reached it’s end-of-life and updating to OpenSAML 4.x is recommended. For that reason, Spring Security supports both OpenSAML 3.x and 4.x. If you manage your OpenSAML dependency to 4.x, then Spring Security will select its OpenSAML 4.x implementations. |
Response
This figure builds off of the Saml2WebSsoAuthenticationFilter
diagram.
The Saml2WebSsoAuthenticationFilter
formulates the Saml2AuthenticationToken
and invokes the AuthenticationManager
.
The AuthenticationManager
invokes the OpenSAML authentication provider.
The authentication provider deserializes the response into an OpenSAML Response
and checks its signature.
If the signature is invalid, authentication fails.
Then, the provider decrypts any EncryptedAssertion
elements.
If any decryptions fail, authentication fails.
Next, the provider validates the response’s Issuer
and Destination
values.
If they don’t match what’s in the RelyingPartyRegistration
, authentication fails.
After that, the provider verifies the signature of each Assertion
.
If any signature is invalid, authentication fails.
Also, if neither the response nor the assertions have signatures, authentication fails.
Either the response or all the assertions must have signatures.
Then, the provider decrypts any EncryptedID
or EncryptedAttribute
elements.
If any decryptions fail, authentication fails.
Next, the provider validates each assertion’s ExpiresAt
and NotBefore
timestamps, the <Subject>
and any <AudienceRestriction>
conditions.
If any validations fail, authentication fails.
Following that, the provider takes the first assertion’s AttributeStatement
and maps it to a Map<String, List<Object>>
.
It also grants the ROLE_USER
granted authority.
And finally, it takes the NameID
from the first assertion, the Map
of attributes, and the GrantedAuthority
and constructs a Saml2AuthenticatedPrincipal
.
Then, it places that principal and the authorities into a Saml2Authentication
.
The resulting Authentication#getPrincipal
is a Spring Security Saml2AuthenticatedPrincipal
object, and Authentication#getName
maps to the first assertion’s NameID
element.
Customizing OpenSAML Configuration
Any class that uses both Spring Security and OpenSAML should statically initialize OpenSamlInitializationService
at the beginning of the class, like so:
static {
OpenSamlInitializationService.initialize();
}
companion object {
init {
OpenSamlInitializationService.initialize()
}
}
This replaces OpenSAML’s InitializationService#initialize
.
Occasionally, it can be valuable to customize how OpenSAML builds, marshalls, and unmarshalls SAML objects.
In these circumstances, you may instead want to call OpenSamlInitializationService#requireInitialize(Consumer)
that gives you access to OpenSAML’s XMLObjectProviderFactory
.
For example, when sending an unsigned AuthNRequest, you may want to force reauthentication.
In that case, you can register your own AuthnRequestMarshaller
, like so:
static {
OpenSamlInitializationService.requireInitialize(factory -> {
AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
@Override
public Element marshall(XMLObject object, Element element) throws MarshallingException {
configureAuthnRequest((AuthnRequest) object);
return super.marshall(object, element);
}
public Element marshall(XMLObject object, Document document) throws MarshallingException {
configureAuthnRequest((AuthnRequest) object);
return super.marshall(object, document);
}
private void configureAuthnRequest(AuthnRequest authnRequest) {
authnRequest.setForceAuthn(true);
}
}
factory.getMarshallerFactory().registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller);
});
}
companion object {
init {
OpenSamlInitializationService.requireInitialize {
val marshaller = object : AuthnRequestMarshaller() {
override fun marshall(xmlObject: XMLObject, element: Element): Element {
configureAuthnRequest(xmlObject as AuthnRequest)
return super.marshall(xmlObject, element)
}
override fun marshall(xmlObject: XMLObject, document: Document): Element {
configureAuthnRequest(xmlObject as AuthnRequest)
return super.marshall(xmlObject, document)
}
private fun configureAuthnRequest(authnRequest: AuthnRequest) {
authnRequest.isForceAuthn = true
}
}
it.marshallerFactory.registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller)
}
}
}
The requireInitialize
method may only be called once per application instance.
13.1.4. Overriding or Replacing Boot Auto Configuration
There are two @Bean
s that Spring Boot generates for a relying party.
The first is a WebSecurityConfigurerAdapter
that configures the app as a relying party.
When including spring-security-saml2-service-provider
, the WebSecurityConfigurerAdapter
looks like:
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.saml2Login(withDefaults());
}
fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login { }
}
}
If the application doesn’t expose a WebSecurityConfigurerAdapter
bean, then Spring Boot will expose the above default one.
You can replace this by exposing the bean within the application:
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
.anyRequest().authenticated()
)
.saml2Login(withDefaults());
}
}
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("ROLE_USER"))
authorize(anyRequest, authenticated)
}
saml2Login {
}
}
}
}
The above requires the role of USER
for any URL that starts with /messages/
.
The second @Bean
Spring Boot creates is a RelyingPartyRegistrationRepository
, which represents the asserting party and relying party metadata.
This includes things like the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party.
You can override the default by publishing your own RelyingPartyRegistrationRepository
bean.
For example, you can look up the asserting party’s configuration by hitting its metadata endpoint like so:
@Value("${metadata.location}")
String assertingPartyMetadataLocation;
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation(assertingPartyMetadataLocation)
.registrationId("example")
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Value("\${metadata.location}")
var assertingPartyMetadataLocation: String? = null
@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
val registration = RelyingPartyRegistrations
.fromMetadataLocation(assertingPartyMetadataLocation)
.registrationId("example")
.build()
return InMemoryRelyingPartyRegistrationRepository(registration)
}
Or you can provide each detail manually, as you can see below:
@Value("${verification.key}")
File verificationKey;
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
X509Certificate certificate = X509Support.decodeCertificate(this.verificationKey);
Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
RelyingPartyRegistration registration = RelyingPartyRegistration
.withRegistrationId("example")
.assertingPartyDetails(party -> party
.entityId("https://idp.example.com/issuer")
.singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
.wantAuthnRequestsSigned(false)
.verificationX509Credentials(c -> c.add(credential))
)
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Value("\${verification.key}")
var verificationKey: File? = null
@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
val registration = RelyingPartyRegistration
.withRegistrationId("example")
.assertingPartyDetails { party: AssertingPartyDetails.Builder ->
party
.entityId("https://idp.example.com/issuer")
.singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
.wantAuthnRequestsSigned(false)
.verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
c.add(
credential
)
}
}
.build()
return InMemoryRelyingPartyRegistrationRepository(registration)
}
Note that X509Support is an OpenSAML class, used here in the snippet for brevity
|
Alternatively, you can directly wire up the repository using the DSL, which will also override the auto-configured WebSecurityConfigurerAdapter
:
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.relyingPartyRegistrationRepository(relyingPartyRegistrations())
);
}
}
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("ROLE_USER"))
authorize(anyRequest, authenticated)
}
saml2Login {
relyingPartyRegistrationRepository = relyingPartyRegistrations()
}
}
}
}
A relying party can be multi-tenant by registering more than one relying party in the RelyingPartyRegistrationRepository .
|
13.1.5. RelyingPartyRegistration
A RelyingPartyRegistration
instance represents a link between an relying party and assering party’s metadata.
In a RelyingPartyRegistration
, you can provide relying party metadata like its Issuer
value, where it expects SAML Responses to be sent to, and any credentials that it owns for the purposes of signing or decrypting payloads.
Also, you can provide asserting party metadata like its Issuer
value, where it expects AuthnRequests to be sent to, and any public credentials that it owns for the purposes of the relying party verifying or encrypting payloads.
The following RelyingPartyRegistration
is the minimum required for most setups:
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata")
.registrationId("my-id")
.build();
val relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata")
.registrationId("my-id")
.build()
Note that you can also create a RelyingPartyRegistration
from an arbitrary InputStream
source.
One such example is when the metadata is stored in a database:
String xml = fromDatabase();
try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadata(source)
.registrationId("my-id")
.build();
}
Though a more sophisticated setup is also possible, like so:
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
.entityId("{baseUrl}/{registrationId}")
.decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
.assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
.assertingPartyDetails(party -> party
.entityId("https://ap.example.org")
.verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
.singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
)
.build();
val relyingPartyRegistration =
RelyingPartyRegistration.withRegistrationId("my-id")
.entityId("{baseUrl}/{registrationId}")
.decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
c.add(relyingPartyDecryptingCredential())
}
.assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
.assertingPartyDetails { party -> party
.entityId("https://ap.example.org")
.verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
.singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
}
.build()
The top-level metadata methods are details about the relying party.
The methods inside assertingPartyDetails are details about the asserting party.
|
The location where a relying party is expecting SAML Responses is the Assertion Consumer Service Location. |
The default for the relying party’s entityId
is {baseUrl}/saml2/service-provider-metadata/{registrationId}
.
This is this value needed when configuring the asserting party to know about your relying party.
The default for the assertionConsumerServiceLocation
is /login/saml2/sso/{registrationId}
.
It’s mapped by default to Saml2WebSsoAuthenticationFilter
in the filter chain.
URI Patterns
You probably noticed in the above examples the {baseUrl}
and {registrationId}
placeholders.
These are useful for generating URIs. As such, the relying party’s entityId
and assertionConsumerServiceLocation
support the following placeholders:
-
baseUrl
- the scheme, host, and port of a deployed application -
registrationId
- the registration id for this relying party -
baseScheme
- the scheme of a deployed application -
baseHost
- the host of a deployed application -
basePort
- the port of a deployed application
For example, the assertionConsumerServiceLocation
defined above was:
/my-login-endpoint/{registrationId}
which in a deployed application would translate to
/my-login-endpoint/adfs
The entityId
above was defined as:
{baseUrl}/{registrationId}
which in a deployed application would translate to
https://rp.example.com/adfs
Credentials
You also likely noticed the credential that was used.
Oftentimes, a relying party will use the same key to sign payloads as well as decrypt them. Or it will use the same key to verify payloads as well as encrypt them.
Because of this, Spring Security ships with Saml2X509Credential
, a SAML-specific credential that simplifies configuring the same key for different use cases.
At a minimum, it’s necessary to have a certificate from the asserting party so that the asserting party’s signed responses can be verified.
To construct a Saml2X509Credential
that you’ll use to verify assertions from the asserting party, you can load the file and use
the CertificateFactory
like so:
Resource resource = new ClassPathResource("ap.crt");
try (InputStream is = resource.getInputStream()) {
X509Certificate certificate = (X509Certificate)
CertificateFactory.getInstance("X.509").generateCertificate(is);
return Saml2X509Credential.verification(certificate);
}
val resource = ClassPathResource("ap.crt")
resource.inputStream.use {
return Saml2X509Credential.verification(
CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
)
}
Let’s say that the asserting party is going to also encrypt the assertion. In that case, the relying party will need a private key to be able to decrypt the encrypted value.
In that case, you’ll need an RSAPrivateKey
as well as its corresponding X509Certificate
.
You can load the first using Spring Security’s RsaKeyConverters
utility class and the second as you did before:
X509Certificate certificate = relyingPartyDecryptionCertificate();
Resource resource = new ClassPathResource("rp.crt");
try (InputStream is = resource.getInputStream()) {
RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
return Saml2X509Credential.decryption(rsa, certificate);
}
val certificate: X509Certificate = relyingPartyDecryptionCertificate()
val resource = ClassPathResource("rp.crt")
resource.inputStream.use {
val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
return Saml2X509Credential.decryption(rsa, certificate)
}
When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you. |
Resolving the Relying Party from the Request
As seen so far, Spring Security resolves the RelyingPartyRegistration
by looking for the registration id in the URI path.
There are a number of reasons you may want to customize. Among them:
-
You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme
-
You may identify tenants in a way other than by the URI path
To customize the way that a RelyingPartyRegistration
is resolved, you can configure a custom Converter<HttpServletRequest, RelyingPartyRegistration>
.
The default looks up the registration id from the URI’s last path element and looks it up in your RelyingPartyRegistrationRepository
.
You can provide a simpler resolver that, for example, always returns the same relying party:
public class SingleRelyingPartyRegistrationResolver
implements Converter<HttpServletRequest, RelyingPartyRegistration> {
@Override
public RelyingPartyRegistration convert(HttpServletRequest request) {
return this.relyingParty;
}
}
class SingleRelyingPartyRegistrationResolver : Converter<HttpServletRequest?, RelyingPartyRegistration?> {
override fun convert(request: HttpServletRequest?): RelyingPartyRegistration? {
return this.relyingParty
}
}
Then, you can provide this resolver to the appropriate filters that produce <saml2:AuthnRequest>
s, authenticate <saml2:Response>
s, and produce <saml2:SPSSODescriptor>
metadata.
Remember that if you have any placeholders in your RelyingPartyRegistration , your resolver implementation should resolve them.
|
Duplicated Relying Party Configurations
When an application uses multiple asserting parties, some configuration is duplicated between RelyingPartyRegistration
instances:
-
The relying party’s
entityId
-
Its
assertionConsumerServiceLocation
, and -
Its credentials, for example its signing or decryption credentials
What’s nice about this setup is credentials may be more easily rotated for some identity providers vs others.
The duplication can be alleviated in a few different ways.
First, in YAML this can be alleviated with references, like so:
spring:
security:
saml2:
relyingparty:
okta:
signing.credentials: &relying-party-credentials
- private-key-location: classpath:rp.key
- certificate-location: classpath:rp.crt
identityprovider:
entity-id: ...
azure:
signing.credentials: *relying-party-credentials
identityprovider:
entity-id: ...
Second, in a database, it’s not necessary to replicate RelyingPartyRegistration
's model.
Third, in Java, you can create a custom configuration method, like so:
private RelyingPartyRegistration.Builder
addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {
Saml2X509Credential signingCredential = ...
builder.signingX509Credentials(c -> c.addAll(signingCredential));
// ... other relying party configurations
}
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
RelyingPartyRegistration okta = addRelyingPartyDetails(
RelyingPartyRegistrations
.fromMetadataLocation(oktaMetadataUrl)
.registrationId("okta")).build();
RelyingPartyRegistration azure = addRelyingPartyDetails(
RelyingPartyRegistrations
.fromMetadataLocation(oktaMetadataUrl)
.registrationId("azure")).build();
return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
}
private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
val signingCredential: Saml2X509Credential = ...
builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
c.add(
signingCredential
)
}
// ... other relying party configurations
}
@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
val okta = addRelyingPartyDetails(
RelyingPartyRegistrations
.fromMetadataLocation(oktaMetadataUrl)
.registrationId("okta")
).build()
val azure = addRelyingPartyDetails(
RelyingPartyRegistrations
.fromMetadataLocation(oktaMetadataUrl)
.registrationId("azure")
).build()
return InMemoryRelyingPartyRegistrationRepository(okta, azure)
}
13.1.6. Producing <saml2:AuthnRequest>
s
As stated earlier, Spring Security’s SAML 2.0 support produces a <saml2:AuthnRequest>
to commence authentication with the asserting party.
Spring Security achieves this in part by registering the Saml2WebSsoAuthenticationRequestFilter
in the filter chain.
This filter by default responds to endpoint /saml2/authenticate/{registrationId}
.
For example, if you were deployed to https://rp.example.com
and you gave your registration an ID of okta
, you could navigate to:
and the result would be a redirect that included a SAMLRequest
parameter containing the signed, deflated, and encoded <saml2:AuthnRequest>
.
Changing How the <saml2:AuthnRequest>
Gets Sent
By default, Spring Security signs each <saml2:AuthnRequest>
and send it as a GET to the asserting party.
Many asserting parties don’t require a signed <saml2:AuthnRequest>
.
This can be configured automatically via RelyingPartyRegistrations
, or you can supply it manually, like so:
spring:
security:
saml2:
relyingparty:
okta:
identityprovider:
entity-id: ...
singlesignon.sign-request: false
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails(party -> party
// ...
.wantAuthnRequestsSigned(false)
)
.build();
var relyingPartyRegistration: RelyingPartyRegistration =
RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
// ...
.wantAuthnRequestsSigned(false)
}
.build();
Otherwise, you will need to specify a private key to RelyingPartyRegistration#signingX509Credentials
so that Spring Security can sign the <saml2:AuthnRequest>
before sending.
By default, Spring Security will sign the <saml2:AuthnRequest>
using rsa-sha256
, though some asserting parties will require a different algorithm, as indicated in their metadata.
You can configure the algorithm based on the asserting party’s metadata using RelyingPartyRegistrations
.
Or, you can provide it manually:
String metadataLocation = "classpath:asserting-party-metadata.xml";
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
// ...
.assertingPartyDetails((party) -> party
// ...
.signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512))
)
.build();
var metadataLocation = "classpath:asserting-party-metadata.xml"
var relyingPartyRegistration: RelyingPartyRegistration =
RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
// ...
.signingAlgorithms { sign: MutableList<String?> ->
sign.add(
SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512
)
}
}
.build();
The snippet above uses the OpenSAML SignatureConstants class to supply the algorithm name.
But, that’s just for convenience.
Since the datatype is String , you can supply the name of the algorithm directly.
|
Some asserting parties require that the <saml2:AuthnRequest>
be POSTed.
This can be configured automatically via RelyingPartyRegistrations
, or you can supply it manually, like so:
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails(party -> party
// ...
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
)
.build();
var relyingPartyRegistration: RelyingPartyRegistration? =
RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
// ...
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
}
.build()
Customizing OpenSAML’s AuthnRequest
Instance
There are a number of reasons that you may want to adjust an AuthnRequest
.
For example, you may want ForceAuthN
to be set to true
, which Spring Security sets to false
by default.
If you don’t need information from the HttpServletRequest
to make your decision, then the easiest way is to register a custom AuthnRequestMarshaller
with OpenSAML.
This will give you access to post-process the AuthnRequest
instance before it’s serialized.
But, if you do need something from the request, then you can use create a custom Saml2AuthenticationRequestContext
implementation and then a Converter<Saml2AuthenticationRequestContext, AuthnRequest>
to build an AuthnRequest
yourself, like so:
@Component
public class AuthnRequestConverter implements
Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
private final AuthnRequestBuilder authnRequestBuilder;
private final IssuerBuilder issuerBuilder;
// ... constructor
public AuthnRequest convert(Saml2AuthenticationRequestContext context) {
MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context;
Issuer issuer = issuerBuilder.buildObject();
issuer.setValue(myContext.getIssuer());
AuthnRequest authnRequest = authnRequestBuilder.buildObject();
authnRequest.setIssuer(issuer);
authnRequest.setDestination(myContext.getDestination());
authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl());
// ... additional settings
authRequest.setForceAuthn(myContext.getForceAuthn());
return authnRequest;
}
}
@Component
class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
private val authnRequestBuilder: AuthnRequestBuilder? = null
private val issuerBuilder: IssuerBuilder? = null
// ... constructor
override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
val myContext: MySaml2AuthenticationRequestContext = context
val issuer: Issuer = issuerBuilder.buildObject()
issuer.value = myContext.getIssuer()
val authnRequest: AuthnRequest = authnRequestBuilder.buildObject()
authnRequest.issuer = issuer
authnRequest.destination = myContext.getDestination()
authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl()
// ... additional settings
authRequest.setForceAuthn(myContext.getForceAuthn())
return authnRequest
}
}
Then, you can construct your own Saml2AuthenticationRequestContextResolver
and Saml2AuthenticationRequestFactory
and publish them as @Bean
s:
@Bean
Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() {
Saml2AuthenticationRequestContextResolver resolver =
new DefaultSaml2AuthenticationRequestContextResolver();
return request -> {
Saml2AuthenticationRequestContext context = resolver.resolve(request);
return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null);
};
}
@Bean
Saml2AuthenticationRequestFactory authenticationRequestFactory(
AuthnRequestConverter authnRequestConverter) {
OpenSaml4AuthenticationRequestFactory authenticationRequestFactory =
new OpenSaml4AuthenticationRequestFactory();
authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter);
return authenticationRequestFactory;
}
@Bean
open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver {
val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver()
return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest ->
val context = resolver.resolve(request)
MySaml2AuthenticationRequestContext(
context,
request.getParameter("force") != null
)
}
}
@Bean
open fun authenticationRequestFactory(
authnRequestConverter: AuthnRequestConverter?
): Saml2AuthenticationRequestFactory? {
val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory()
authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter)
return authenticationRequestFactory
}
13.1.7. Authenticating <saml2:Response>
s
To verify SAML 2.0 Responses, Spring Security uses OpenSaml4AuthenticationProvider
by default.
You can configure this in a number of ways including:
-
Setting a clock skew to timestamp validation
-
Mapping the response to a list of
GrantedAuthority
instances -
Customizing the strategy for validating assertions
-
Customizing the strategy for decrypting response and assertion elements
To configure these, you’ll use the saml2Login#authenticationManager
method in the DSL.
Setting a Clock Skew
It’s not uncommon for the asserting and relying parties to have system clocks that aren’t perfectly synchronized.
For that reason, you can configure OpenSaml4AuthenticationProvider
's default assertion validator with some tolerance:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
.createDefaultAssertionValidator(assertionToken -> {
Map<String, Object> params = new HashMap<>();
params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
// ... other validation parameters
return new ValidationContext(params);
})
);
http
.authorizeRequests(authz -> authz
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.authenticationManager(new ProviderManager(authenticationProvider))
);
}
}
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
val authenticationProvider = OpenSaml4AuthenticationProvider()
authenticationProvider.setAssertionValidator(
OpenSaml4AuthenticationProvider
.createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
val params: MutableMap<String, Any> = HashMap()
params[CLOCK_SKEW] =
Duration.ofMinutes(10).toMillis()
ValidationContext(params)
})
)
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = ProviderManager(authenticationProvider)
}
}
}
}
Coordinating with a UserDetailsService
Or, perhaps you would like to include user details from a legacy UserDetailsService
.
In that case, the response authentication converter can come in handy, as can be seen below:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
Saml2Authentication authentication = OpenSaml4AuthenticationProvider
.createDefaultResponseAuthenticationConverter() (1)
.convert(responseToken);
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
String username = assertion.getSubject().getNameID().getValue();
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); (2)
return MySaml2Authentication(userDetails, authentication); (3)
});
http
.authorizeRequests(authz -> authz
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.authenticationManager(new ProviderManager(authenticationProvider))
);
}
}
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
@Autowired
var userDetailsService: UserDetailsService? = null
override fun configure(http: HttpSecurity) {
val authenticationProvider = OpenSaml4AuthenticationProvider()
authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
val authentication = OpenSaml4AuthenticationProvider
.createDefaultResponseAuthenticationConverter() (1)
.convert(responseToken)
val assertion: Assertion = responseToken.response.assertions[0]
val username: String = assertion.subject.nameID.value
val userDetails = userDetailsService!!.loadUserByUsername(username) (2)
MySaml2Authentication(userDetails, authentication) (3)
}
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = ProviderManager(authenticationProvider)
}
}
}
}
1 | First, call the default converter, which extracts attributes and authorities from the response |
2 | Second, call the UserDetailsService using the relevant information |
3 | Third, return a custom authentication that includes the user details |
It’s not required to call OpenSaml4AuthenticationProvider 's default authentication converter.
It returns a Saml2AuthenticatedPrincipal containing the attributes it extracted from AttributeStatement s as well as the single ROLE_USER authority.
|
Performing Additional Response Validation
OpenSaml4AuthenticationProvider
validates the Issuer
and Destination
values right after decrypting the Response
.
You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.
For example, you can throw a custom exception with any additional information available in the Response
object, like so:
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseValidator((responseToken) -> {
Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
.createDefaultResponseValidator()
.convert(responseToken)
.concat(myCustomValidator.convert(responseToken));
if (!result.getErrors().isEmpty()) {
String inResponseTo = responseToken.getInResponseTo();
throw new CustomSaml2AuthenticationException(result, inResponseTo);
}
return result;
});
Performing Additional Assertion Validation
OpenSaml4AuthenticationProvider
performs minimal validation on SAML 2.0 Assertions.
After verifying the signature, it will:
-
Validate
<AudienceRestriction>
and<DelegationRestriction>
conditions -
Validate
<SubjectConfirmation>
s, expect for any IP address information
To perform additional validation, you can configure your own assertion validator that delegates to OpenSaml4AuthenticationProvider
's default and then performs its own.
For example, you can use OpenSAML’s OneTimeUseConditionValidator
to also validate a <OneTimeUse>
condition, like so:
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
provider.setAssertionValidator(assertionToken -> {
Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
.createDefaultAssertionValidator()
.convert(assertionToken);
Assertion assertion = assertionToken.getAssertion();
OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
ValidationContext context = new ValidationContext();
try {
if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
return result;
}
} catch (Exception e) {
return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
}
return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
});
var provider = OpenSaml4AuthenticationProvider()
var validator: OneTimeUseConditionValidator = ...
provider.setAssertionValidator { assertionToken ->
val result = OpenSaml4AuthenticationProvider
.createDefaultAssertionValidator()
.convert(assertionToken)
val assertion: Assertion = assertionToken.assertion
val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
val context = ValidationContext()
try {
if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
return@setAssertionValidator result
}
} catch (e: Exception) {
return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
}
result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
}
While recommended, it’s not necessary to call OpenSaml4AuthenticationProvider 's default assertion validator.
A circumstance where you would skip it would be if you don’t need it to check the <AudienceRestriction> or the <SubjectConfirmation> since you are doing those yourself.
|
Customizing Decryption
Spring Security decrypts <saml2:EncryptedAssertion>
, <saml2:EncryptedAttribute>
, and <saml2:EncryptedID>
elements automatically by using the decryption Saml2X509Credential
instances registered in the RelyingPartyRegistration
.
OpenSaml4AuthenticationProvider
exposes two decryption strategies.
The response decrypter is for decrypting encrypted elements of the <saml2:Response>
, like <saml2:EncryptedAssertion>
.
The assertion decrypter is for decrypting encrypted elements of the <saml2:Assertion>
, like <saml2:EncryptedAttribute>
and <saml2:EncryptedID>
.
You can replace OpenSaml4AuthenticationProvider’s default decryption strategy with your own.
For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>
, you can use it instead like so:
MyDecryptionService decryptionService = ...;
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml4AuthenticationProvider()
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
If you are also decrypting individual elements in a <saml2:Assertion>
, you can customize the assertion decrypter, too:
provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
There are two separate decrypters since assertions can be signed separately from responses. Trying to decrypt a signed assertion’s elements before signature verification may invalidate the signature. If your asserting party signs the response only, then it’s safe to decrypt all elements using only the response decrypter. |
Using a Custom Authentication Manager
Of course, the authenticationManager
DSL method can be also used to perform a completely custom SAML 2.0 authentication.
This authentication manager should expect a Saml2AuthenticationToken
object containing the SAML 2.0 Response XML data.
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.authenticationManager(authenticationManager)
)
;
}
}
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = customAuthenticationManager
}
}
}
}
13.1.8. Using Saml2AuthenticatedPrincipal
With the relying party correctly configured for a given asserting party, it’s ready to accept assertions.
Once the relying party validates an assertion, the result is a Saml2Authentication
with a Saml2AuthenticatedPrincipal
.
This means that you can access the principal in your controller like so:
@Controller
public class MainController {
@GetMapping("/")
public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
String email = principal.getFirstAttribute("email");
model.setAttribute("email", email);
return "index";
}
}
@Controller
class MainController {
@GetMapping("/")
fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
val email = principal.getFirstAttribute<String>("email")
model.setAttribute("email", email)
return "index"
}
}
Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call getAttribute to get the list of attributes or getFirstAttribute to get the first in the list.
getFirstAttribute is quite handy when you know that there is only one value.
|
13.1.9. Producing <saml2:SPSSODescriptor>
Metadata
You can publish a metadata endpoint by adding the Saml2MetadataFilter
to the filter chain, as you’ll see below:
Converter<HttpServletRequest, RelyingPartyRegistration> relyingPartyRegistrationResolver =
new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository);
Saml2MetadataFilter filter = new Saml2MetadataFilter(
relyingPartyRegistrationResolver,
new OpenSamlMetadataResolver());
http
// ...
.saml2Login(withDefaults())
.addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
val relyingPartyRegistrationResolver: Converter<HttpServletRequest, RelyingPartyRegistration> =
DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)
val filter = Saml2MetadataFilter(
relyingPartyRegistrationResolver,
OpenSamlMetadataResolver()
)
http {
//...
saml2Login { }
addFilterBefore<Saml2WebSsoAuthenticationFilter>(filter)
}
You can use this metadata endpoint to register your relying party with your asserting party. This is often as simple as finding the correct form field to supply the metadata endpoint.
By default, the metadata endpoint is /saml2/service-provider-metadata/{registrationId}
.
You can change this by calling the setRequestMatcher
method on the filter:
filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"));
filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"))
ensuring that the registrationId
hint is at the end of the path.
Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a registrationId
hint, like so:
filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
13.1.10. Performing Single Logout
Spring Security does not yet support single logout.
Generally speaking, though, you can achieve this by creating and registering a custom LogoutSuccessHandler
and RequestMatcher
:
http
// ...
.logout(logout -> logout
.logoutSuccessHandler(myCustomSuccessHandler())
.logoutRequestMatcher(myRequestMatcher())
)
http {
logout {
// ...
logoutSuccessHandler = myCustomSuccessHandler()
logoutRequestMatcher = myRequestMatcher()
}
}
The success handler will send logout requests to the asserting party.
The request matcher will detect logout requests from the asserting party.
14. Protection Against Exploits
14.1. Cross Site Request Forgery (CSRF) for Servlet Environments
This section discusses Spring Security’s Cross Site Request Forgery (CSRF) support for servlet environments.
14.1.1. Using Spring Security CSRF Protection
The steps to using Spring Security’s CSRF protection are outlined below:
Use proper HTTP verbs
The first step to protecting against CSRF attacks is to ensure your website uses proper HTTP verbs. This is covered in detail in Safe Methods Must be Idempotent.
Configure CSRF Protection
The next step is to configure Spring Security’s CSRF protection within your application. Spring Security’s CSRF protection is enabled by default, but you may need to customize the configuration. Below are a few common customizations.
Custom CsrfTokenRepository
By default Spring Security stores the expected CSRF token in the HttpSession
using HttpSessionCsrfTokenRepository
.
There can be cases where users will want to configure a custom CsrfTokenRepository
.
For example, it might be desirable to persist the CsrfToken
in a cookie to support a JavaScript based application.
By default the CookieCsrfTokenRepository
will write to a cookie named XSRF-TOKEN
and read it from a header named X-XSRF-TOKEN
or the HTTP parameter _csrf
.
These defaults come from AngularJS
You can configure CookieCsrfTokenRepository
in XML using the following:
<http>
<!-- ... -->
<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/>
The sample explicitly sets |
You can configure CookieCsrfTokenRepository
in Java Configuration using:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
}
}
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
csrf {
csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()
}
}
}
}
The sample explicitly sets |
Disable CSRF Protection
CSRF protection is enabled by default. However, it is simple to disable CSRF protection if it makes sense for your application.
The XML configuration below will disable CSRF protection.
<http>
<!-- ... -->
<csrf disabled="true"/>
</http>
The Java configuration below will disable CSRF protection.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http
.csrf(csrf -> csrf.disable());
}
}
@Configuration
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
csrf {
disable()
}
}
}
}
Include the CSRF Token
In order for the synchronizer token pattern to protect against CSRF attacks, we must include the actual CSRF token in the HTTP request. This must be included in a part of the request (i.e. form parameter, HTTP header, etc) that is not automatically included in the HTTP request by the browser.
Spring Security’s CsrfFilter exposes a CsrfToken as an HttpServletRequest
attribute named _csrf
.
This means that any view technology can access the CsrfToken
to expose the expected token as either a form or meta tag.
Fortunately, there are integrations listed below that make including the token in form and ajax requests even easier.
Form URL Encoded
In order to post an HTML form the CSRF token must be included in the form as a hidden input. For example, the rendered HTML might look like:
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
Next we will discuss various ways of including the CSRF token in a form as a hidden input.
Spring Security’s CSRF support provides integration with Spring’s RequestDataValueProcessor via its CsrfRequestDataValueProcessor.
This means that if you leverage Spring’s form tag library, Thymeleaf, or any other view technology that integrates with RequestDataValueProcessor
, then forms that have an unsafe HTTP method (i.e. post) will automatically include the actual CSRF token.
If you are using JSPs, then you can use Spring’s form tag library. However, if that is not an option, you can also easily include the token with the csrfInput tag.
If the other options for including the actual CSRF token in the request do not work, you can take advantage of the fact that the CsrfToken
is exposed as an HttpServletRequest
attribute named _csrf
.
An example of doing this with a JSP is shown below:
<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
method="post">
<input type="submit"
value="Log out" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
</form>
Ajax and JSON Requests
If you are using JSON, then it is not possible to submit the CSRF token within an HTTP parameter. Instead you can submit the token within a HTTP header.
In the following sections we will discuss various ways of including the CSRF token as an HTTP request header in JavaScript based applications.
Spring Security can easily be configured to store the expected CSRF token in a cookie. By storing the expected CSRF in a cookie, JavaScript frameworks like AngularJS will automatically include the actual CSRF token in the HTTP request headers.
An alternative pattern to exposing the CSRF in a cookie is to include the CSRF token within your meta
tags.
The HTML might look something like this:
<html>
<head>
<meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<meta name="_csrf_header" content="X-CSRF-TOKEN"/>
<!-- ... -->
</head>
<!-- ... -->
Once the meta tags contained the CSRF token, the JavaScript code would read the meta tags and include the CSRF token as a header. If you were using jQuery, this could be done with the following:
$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});
If you are using JSPs a simple way to write the CSRF token to the meta
tags is by leveraging the csrfMeta tag.
If the other options for including the actual CSRF token in the request do not work, you can take advantage of the fact that the CsrfToken
is exposed as an HttpServletRequest
attribute named _csrf
.
An example of doing this with a JSP is shown below:
<html>
<head>
<meta name="_csrf" content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<!-- ... -->
</head>
<!-- ... -->
14.1.2. CSRF Considerations
There are a few special considerations to consider when implementing protection against CSRF attacks. This section discusses those considerations as it pertains to servlet environments. Refer to CSRF Considerations for a more general discussion.
Logging In
It is important to require CSRF for log in requests to protect against forging log in attempts. Spring Security’s servlet support does this out of the box.
Logging Out
It is important to require CSRF for log out requests to protect against forging log out attempts.
If CSRF protection is enabled (default), Spring Security’s LogoutFilter
to only process HTTP POST.
This ensures that log out requires a CSRF token and that a malicious user cannot forcibly log out your users.
The easiest approach is to use a form to log out. If you really want a link, you can use JavaScript to have the link perform a POST (i.e. maybe on a hidden form). For browsers with JavaScript that is disabled, you can optionally have the link take the user to a log out confirmation page that will perform the POST.
If you really want to use HTTP GET with logout you can do so, but remember this is generally not recommended.
For example, the following Java Configuration will perform logout with the URL /logout
is requested with any HTTP method:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http
.logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
);
}
}
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
logout {
logoutRequestMatcher = AntPathRequestMatcher("/logout")
}
}
}
}
CSRF and Session Timeouts
By default Spring Security stores the CSRF token in the HttpSession
.
This can lead to a situation where the session expires which means there is not an expected CSRF token to validate against.
We’ve already discussed general solutions to session timeouts. This section discusses the specifics of CSRF timeouts as it pertains to the servlet support.
It is simple to change storage of the expected CSRF token to be in a cookie. For details, refer to the Custom CsrfTokenRepository section.
If a token does expire, you might want to customize how it is handled by specifying a custom AccessDeniedHandler
.
The custom AccessDeniedHandler
can process the InvalidCsrfTokenException
any way you like.
For an example of how to customize the AccessDeniedHandler
refer to the provided links for both xml and Java configuration.
Multipart (file upload)
We have already discussed how protecting multipart requests (file uploads) from CSRF attacks causes a chicken and the egg problem. This section discusses how to implement placing the CSRF token in the body and url within a servlet application.
More information about using multipart forms with Spring can be found within the 1.1.11. Multipart Resolver section of the Spring reference and the MultipartFilter javadoc. |
Place CSRF Token in the Body
We have already discussed the tradeoffs of placing the CSRF token in the body. In this section we will discuss how to configure Spring Security to read the CSRF from the body.
In order to read the CSRF token from the body, the MultipartFilter
is specified before the Spring Security filter.
Specifying the MultipartFilter
before the Spring Security filter means that there is no authorization for invoking the MultipartFilter
which means anyone can place temporary files on your server.
However, only authorized users will be able to submit a File that is processed by your application.
In general, this is the recommended approach because the temporary file upload should have a negligible impact on most servers.
To ensure MultipartFilter
is specified before the Spring Security filter with java configuration, users can override beforeSpringSecurityFilterChain as shown below:
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
class SecurityApplicationInitializer : AbstractSecurityWebAp