Theme NexT works best with JavaScript enabled
0%

Spring Security (OAuth2) Manual


IMPORTANT:
Some of the content here is a personal summary/abbreviation of contents on the Offical Spring Security Documentation. Feel free to refer to the official site if you think some of the sections written here are not clear.


Spring Security Intro

Spring security is very useful for dealing with authorization, authentication, and protection against common attacks. It provides support for OAuth2, SAML2, and etc. Yet, as the title of this manual suggests, the following sections will focus on using OAuth2 for authentication/authorization.

Technically, you do not necessarily need to use Spring Boot for using Spring Security. However, the sections below do assume that you are having a Spring Boot project to provide the backend services.

If you want more information on using tools other than OAuth2, or that you are not using Spring Boot as the backend API implementations, please visit the Official Reference.

Getting Spring Security

If you are using Spring Security with Spring Boot, getting Spring Security playing in your application is simple.

In summary, all you need to do is

  1. Import the spring-boot-starter-security in your pom.xml
    • You can either manually import the dependency, or select the Spring Security dependency in the Spring Initialzr
    • At this point, we are not yet using the OAuth2 Resource Server and OAuth2 Client yet. If you want, feel free to include those in your pom.xml as well.

The necessary dependency looks like:

1
2
3
4
5
6
7
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>

Again, its version has been already specified in the parent:

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

If you want to use a specific version, you can specify it as well:

1
2
3
4
<properties>
<!-- ... -->
<spring-security.version>WHATEVER-VERSION-YOU-WANT-HERE</spring-security.version>
</properties>

Note:

  • If you use a SNAPSHOT version, you need to ensure that you have the Spring Snapshot repository defined, as the following example shows:

    1
    2
    3
    4
    5
    6
    7
    8
    <repositories>
    <!-- ... possibly other repository elements ... -->
    <repository>
    <id>spring-snapshot</id>
    <name>Spring Snapshot Repository</name>
    <url>https://repo.spring.io/snapshot</url>
    </repository>
    </repositories>

Spring Security 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.

Authentication Support

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.

Spring Security provides built in support for authenticating users. Refer to the sections on authentication for details on what is supported for each stack.

Password Storage and Encoding

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

This section discusses a brief history of password storage: how attackers were able to crack the past systems, and what current strategies are.

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 (e.g. feeding its output as input and adjust how many iterations to occur). 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.

Source: https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/#authentication-password-storage-history

DelegatingPasswordEncoder

Prior to Spring Security 5.0 the default PasswordEncoder was NoOpPasswordEncoder which required plain text passwords. Based upon the Password Storage 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.

1
2
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();

Alternatively, you may create your own custom instance:

1
2
3
4
5
6
7
8
9
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);

Encode with Spring Boot CLI

This can be useful sometimes when you are testing some features of your application and needs a certain password to be encoded without running the entire application. This can be achieved in Spring Boot CLI:

For example:

1
2
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6

However, this seems to only be able to provide hashes of type bcrypt.

Password Storage Format

If you run the passwordEncoder("password") for each of the PasswordEncoder above, you will see the following output format:

1
{id}encodedPassword

For example, encoding the word password would give:

1
2
3
4
5
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

Note:

  • 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 $2a$.

Password Matching

The match() method of the PasswordEncoder takes two arguements: the raw password, and the encoded password (in the format {id}encodedPassword).

For example, if you have the password jason, and the encoded hash of it is {bcrypt}$2a$10$ZBDPr38ZlwyPGe/YNsOSHuBEU5TDeNisCds6zJolVW1pN4YEHb3YS , then you do:

1
passwordEncoder.matches("jason","{bcrypt}$2a$10$ZBDPr38ZlwyPGe/YNsOSHuBEU5TDeNisCds6zJolVW1pN4YEHb3YS"));

This behavior can be customized using DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder).

Note:

  • However, if the {id} you put in as part of the argument does not exist in the list of encoders that you specified in the example above, it will throw an IllegalArgumentException. This behavior can be customized using DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder).

However, if you look into the match() method, you will realize that it checks whether if the password is a match for the hash by first looking at the {id} to determine which encoder to use. This means that if you hashed it with bcrypt somewhere in your code, come back here and put a hash of the same password with the pbkdf2 encoder, it will still give a match.

Password Encoders

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.

1
2
3
4
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = 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 if the Argon2PasswordEncoder requires BouncyCastle.

1
2
3
4
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = 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.

1
2
3
4
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = 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.

1
2
3
4
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = 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.

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.

The following sections discuss:

  • Cross Site Request Forgery (CSRF)
  • Security HTTP Response Headers
  • HTTP

Cross Site Request Forgery

This is caused by the fact that authentication cookies associated with a site gets passed along in your request if you did not logout. This means that a website can forge an identical evil request using the authentication cookie passed along.

For example:

Consider that you are on a banking website, where you submitted a request of transferring money that looks like this:

1
2
3
4
5
6
7
8
9
10
11
<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>

Then the request would look like:

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>

As a result, the same request would be generated, with the same cookie but destination account forged.

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:

Note:

  • Both protections require that Safe Methods Must be Idempotent:
    • In order for either protection against CSRF (mentioned above) 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.

In summary, it works like this:

  1. When a user logged in, the server should generate a unique CSRF token associated with the user
  2. When an HTTP request is submitted, it should also send the CSRF token that along with the request
    • The token should NOT be included in your cookie, or anything that will be automatically included in your browser
    • You can, for example, include the token in your request header or parameter
  3. The server receiving the request will now check that CSRF token against the expected CSRF token. If the value does not match, reject the request.

Just to state the take away message again: 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.

For example, the request sent with CSRF token now will look like:

1
2
3
4
5
6
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

where:

  • 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721 is the CSRF token
Same Site Attribute

An emerging way to protect against CSRF attack 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.

However, since Spring Security does not directly control the creation of the session cookie, it does not provide support for the SameSite attribute. Spring Session provides support for the SameSite attribute in servlet based applications. Spring Framework’s CookieWebSessionIdResolver provides out of the box support for the SameSite attribute in WebFlux based applications.

In summary, this works by:

  1. When the user logged in/get authenticated, the cookie comes along with the authentication specifies where the cookies get sent
    • For example, if specified SameSite=Strict, then the cookie will NOT be passed along to different site/request coming from a different site

An example, HTTP response header with the SameSite attribute might look like:

1
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.

Note:

  • 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.

There are some important considerations that one should be aware about when using SameSite attribute to protect against CSRF attacks.

One 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? It is recommended to use CSRF protection for any request that could be processed by a browser by normal/authenticated users. If you are only creating a service that is used by non-browser clients, you will likely want to disable CSRF protection.

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 executed by:

  • A malicious user performs a CSRF login 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 now associated to the malicious user’s account so the malicious user can login with their own credentials and view the victim’s sensitive information

A possible complication to ensuring login 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. Please read on to the next section for more details.

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 (i.e. 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 (i.e. the timeout) you lose the ability to forcibly terminate the token if it is compromised.

Multipart (File Upload)

This is a concern mainly caused by storing your CSRF token in the request body.

For example, if one of your request is uploading a file, the body of the HTTP request will be read (e.g. the file). However, since request body is also the place you store your CSRF token, this means that your body 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.

However, this is not too bad, as you can configure your program to delete that file once the request is rejected (that you found the CSRF token is invalid).

Yet, one approach is to Include CSRF Token in URL:

  • 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.

Security HTTP 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.

Note:

  • The following sections cover the basics of request headers and their options. To modify/add/remove request headers, please go to the Applications section.

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:

1
2
3
4
5
6
7
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

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:

1
2
3
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0

In order to be secure, Spring Security adds these headers by default. However, if your application provides it’s 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 execute it.

Note:

  • 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 execute 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 execute a XSS attack with it.

Spring Security disables content sniffing by default by adding the following header to HTTP responses:

1
X-Content-Type-Options: nosniff

HTTP Strict Transport Security

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 (i.e. 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):

1
Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload

where:

  • The optional includeSubDomains directive instructs the browser that subdomains (i.e. 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.

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 (video demo). 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:

1
X-Frame-Options: DENY

However, the available options are:

  • deny: This directive stops the site from being rendered in <frame> i.e. site can’t be embedded into other sites.
  • sameorigin: This directive allows the page to be rendered in the frame if and only if the frame has the same origin as the page.
  • allow-from url: This directive has now became obsolete and shouldn’t be used. It is not supported by modern browser. In this the page can be rendered in the <frame> that is originated from specified URL.

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:

1
X-XSS-Protection: 1; mode=block

Content Security Policy

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:

1
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.

1
Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/

where:

  • 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.io/.

However, sometimes for developing processes, you just want to test the filtering mechanism (this header is typically used when experimenting and/or developing security policies for a site.). This can be done with setting Content-Security-Policy-Report-Only instead of Content-Security-Report.

For example, given the following response header, the policy declares that scripts may be loaded from one of two possible sources.

1
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.

An Introduction to Content Security Policy

CSP Guide - Mozilla Developer Network

W3C Candidate Recommendation

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:

1
Referrer-Policy: same-origin

where:

While the above is a common policy used, there are also other polices: https://www.w3.org/TR/referrer-policy/#referrer-policies.

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.

For example:

1
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.

Clear Site Data

Clear Site Data is a mechanism by which any browser-side data related to the site - cookies, local storage, and the like - can be removed when an HTTP response contains this header:

1
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"

This is a nice clean-up action to perform on logout.

Custom Headers

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.

For more details on using custom headers, please visit the Realizations section.

Redirecting HTTP

Redirect to HTTPS

When a client uses HTTP, Spring Security can be configured to redirect to HTTPS. Please see the HTTP Realizations section.

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, or for Spring Boot users, you may use server.use-forward-headers property.

For example, in the former case, Tomcat uses the RemoteIpValve and Jetty uses ForwardedRequestCustomizer. Alternatively, Spring users can leverage ForwardedHeaderFilter.

In the latter case, see the Spring Boot documentation for further details.

Spring Security Project Modules

To see a full list of available modules that Spring Security provides, please visit: https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/#modules.

Servlet Security: The Big Picture

This section discusses Spring Security’s high level architecture within Servlet based applications.

Later sections such as Authentication Realizations, Authorization Realizations, Protection Against Exploits Realizations will build on from this.

A Review of Filters

Spring Security’s Servlet support is based on Servlet Filters, so it is helpful to look at the role of Filters generally first. The picture below shows the typical layering of the handlers for a single HTTP request.

Picture from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/architecture/filterchain.png

The client sends a request to the application, and the container creates a FilterChain which contains the Filters 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, using/having more than one Filter can be used to:

  • Prevent downstream Filters or the Servlet from being invoked. In this instance the Filter will typically write the HttpServletResponse.
  • Modify the HttpServletRequest or HttpServletResponse used by the downstream Filters and Servlet

For example:

1
2
3
4
5
public 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
}

Since a Filter only impacts downstream Filters and the Servlet, the order each Filter is invoked is extremely important.

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 Filters using its own standards, but it is not aware of Spring defined Beans. Therefore, the DelegatingFilterProxy can be useful:

  • 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 Filters and the FilterChain.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/architecture/delegatingfilterproxy.png

The pseudo code for DelegatingFilterProxy could look like:

1
2
3
4
5
6
7
8
9
// inside DelegatingFilterProxy
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean

// For the exampl, delegate is an instance of Bean Filter0
Filter delegate = 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.

FilterChainProxy

Besides registering a filter bean in your DelegatingFilterProxy, you can also register FilterChainProxys, which allow you to add a group of filters inside the chain.

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.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/architecture/filterchainproxy.png

SecurityFilterChain

SecurityFilterChain is used by FilterChainProxy to determine which Spring Security Filters should be invoked for this request.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/architecture/securityfilterchain.png

where:

FilterChainProxy provides a number of advantages to registering directly with the Servlet container or DelegatingFilterProxy.

  1. 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.

  2. 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.

  3. In addition, it provides more flexibility in determining when a SecurityFilterChain should be invoked. In a Servlet container, Filters 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 if your application.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/architecture/multi-securityfilterchain.png

In the above image, FilterChainProxy decides which SecurityFilterChain should be used.

Note:

  • 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 SecurityFilterChain_0 will be invoked even though it also matches on SecurityFilterChain_n. If a URL of /messages/ is requested, it will not match on SecurityFilterChain_0‘s pattern of /api/**, so FilterChainProxy will continue trying each SecurityFilterChain. Assuming that no other, SecurityFilterChain instances match SecurityFilterChain_n will be invoked.

Security Filters

The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. The order of Filters matters. It is typically not necessary to know the ordering of Spring Security’s Filters. However, there are times that it is beneficial to know the ordering.

Below is a comprehensive list of Spring Security Filter ordering that are inserted into the your Spring Boot project:

  • ChannelProcessingFilter
  • ConcurrentSessionFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • CsrfFilter
  • LogoutFilter
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
    • Tries to find a username/password request parameter/POST body and if found, tries to authenticate the user with those values.
  • ConcurrentSessionFilter
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
    • Generates a login page for you, if you don’t explicitly disable that feature. THIS filter is why you get a default login page when enabling Spring Security.
  • DefaultLogoutPageGeneratingFilter
    • Generates a logout page for you, if you don’t explicitly disable that feature.
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
    • Tries to find a Basic Auth HTTP Header on the request and if found, tries to authenticate the user with the header’s username and password.
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
    • See below section
  • FilterSecurityInterceptor
  • SwitchUserFilter

Those filters, for a large part, are Spring Security. Not more, not less. They do all the work. What’s left for you is to configure how they do their work, i.e. which URLs to protect, which to ignore and what database tables to use for authentication.

ExceptionTranslationFilter

The ExceptionTranslationFilter allows translation of AccessDeniedException and AuthenticationException into HTTP responses.

ExceptionTranslationFilter is inserted into the FilterChainProxy as one of the Security Filters.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/architecture/exceptiontranslationfilter.png

where:

  • number 1First, the ExceptionTranslationFilter invokes FilterChain.doFilter(request, response) to invoke the rest of the application.
  • number 2 If the user is not authenticated or it is an AuthenticationException, then Start Authentication.
    • The SecurityContextHolder is cleared out
    • The HttpServletRequest is saved in the RequestCache. When the user successfully authenticates, the RequestCache 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 a WWW-Authenticate header.
  • number 3 Otherwise if it is an AccessDeniedException, then Access Denied. The AccessDeniedHandler is invoked to handle access denied.

However, if the application does not throw an AccessDeniedException or an AuthenticationException, then ExceptionTranslationFilter does not do anything.

The pseudocode for ExceptionTranslationFilter looks something like this:

1
2
3
4
5
6
7
8
9
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException e) {
if (!authenticated || e instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}

Realizations

This section shows you how to implement all the theory mentioned above.

Spring Boot + Spring Security

If you are using Spring Boot (again, this is assumed in this manual), using Spring Security is as simple as importing one more additional dependency in your pom.xml:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

This has already been mentioned in the section Getting Spring Security. For more details, please revisit that section.

Spring Boot Auto Configurations

At this point, Spring Boot has added quite a few into your application.

Spring Boot automatically:

  • Enables Spring Security’s default configuration, which creates a servlet Filter as a bean named springSecurityFilterChain. 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 of user and a randomly generated password that is logged to the console.
  • Registers the Filter with a bean named springSecurityFilterChain with the Servlet container for every request.

Spring Boot is not configuring much, but it does a lot. A summary of the features follows:

Now, if you start your application and send a request, this would be the response header that Spring Security has configured for you.

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/html;charset=UTF-8
Content-Length: 88
Date: Fri, 24 Jul 2020 06:32:43 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Authentication Realizations

Authentication not only means authenticating the user at login time, but also “remembering” the user as being already authenticated when the user made further requests.

In general, there are two ways to achieve authentication: by using a session or by using a token.

  • A session is composed of:
    • sid - session id
    • data - all the information you want to store (e.g. user logged in information)
  • A token is composed of:
    • token - a token that is representing a certain status

Basic Form Authentication

If you are implementing this functionality from scratch (i.e. without Spring Security doing work for you), you need to do quite a lot of work.

In summary, you need to:

  1. Create all the relevant classes, including your @Controller, @Configuration
  2. Create a DTO, which contains fields such as:
    • username
    • password
    • SESSION_ID
  3. Create a service, that achieves:
    • Checking whether the user login information is correct
    • Extracting user information from the SESSION_ID
    • Handling authentication request verification, based on data provided by the object below
      • If verified correctly, creates a session with the specified SESSION_ID and the user DTO as data
      • Provide the SESSION_ID back to be stored in the user’s cookie
  4. Create a request object, that contains:
    • necessary request parameters, such as username and password
  5. Create a controller, that:
    • Uses the request object created above as input argument
    • Uses the service class you created above for handling authentication verification

While the above is achievable and certainly working, things can get complicated when your project scope extends.

With Spring Security, you save a lot of work by:

  • Already having all the authentication/authorization mechanism and filters implemented (see Security Filters).

Therefore, all you need to do is to configure your Spring Security.

Configuring your Spring Security

With the latest Spring Security and/or Spring Boot versions, configuring Spring Security is simple.

In summary, you just need to:

  1. Create a class annotated with @EnableWebSecurity
    • You might want to make it a @Configuration as well, as it configures your application
  2. Extends WebSecurityConfigurer
    • This basically offers you a configuration DSL/methods. With those methods, you can specify what URIs in your application to protect or what exploit protections to enable/disable.
  3. Use the configure() method and specify the configurations you want

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* By overriding the adapter’s configure(HttpSecurity) method, you get a nice little DSL with which you can configure your FilterChain.
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() //1
.antMatchers("/","home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin() //2
.loginPage("/login")
.permitAll()
.and()
.logout() //3
.permitAll()
.and()
.httpBasic(); //4
}
}

where:

  1. authorizeRequests() authorizes request that match certain condition to be passed through
    • for all options you can specify with it, you can look at its class ExpressionUrlAuthorizationConfigurer
  2. formLogin() configures the login request, allowing form login
    • for all options you can specify with it, you can look at its class FormLoginConfigurer
  3. logout() configures the logout request
    • for all options you can specify with it, you can look at its class LogoutConfigurer
  4. httpBasic() allows Basic Auth, i.e. sending in an HTTP Basic Auth Header to authenticate
    • for all options you can specify with it, you can look at its classHttpBasicConfigurer

In fact, the Spring Security implementation has:

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class WebSecurityConfigurerAdapter implements
WebSecurityConfigurer<WebSecurity> {

protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated() // (1)
.and()
.formLogin().and() // (2), uses generated login page
.httpBasic(); // (3)
}
}

where we see the same elements as above, which explains why, if we did not configure anything, we will always be redirected to a generated login page.

Authentication with Spring Security

When it comes to authentication and Spring Security you have roughly three scenarios:

  1. The default: You can access the (hashed) password of the user, because you have his details (username, password) saved in e.g. a database table.
  2. Less common: You cannot access the (hashed) password of the user. This is the case if your users and passwords are stored somewhere else, like in a 3rd party identity management product offering REST services for authentication. Think: Atlassian Crowd.
  3. Also popular: You want to use OAuth2 or “Login with Google/Twitter/etc.” (OpenID), likely in combination with JWT. Then none of the following applies and you should go straight to the OAuth2 chapter.

The following sections will only discuss the default strategy and the OAuth2 strategy, which has its own section. If you need some information on the second topic, please visit https://www.marcobehler.com/guides/spring-security.


In summary, to use the default Spring Security procedure (with some customization), you will need to:

  1. Use the config() method inside a class that extends WebSecurityConfigurerAdapter to configure your Spring Security
  2. Implement the loadUserByUsername method by creating a class that implements UserDetailsService
    • At this point, you need to also create a class that implements UserDetails, which is returned by the above method
    • Since UserDetails needs data (e.g. username/hashed password) from the database, you need to create a service class that achieves this
  3. Choose and create your PasswordEncoder to be injected as a bean
    • This will be the default encoder that you will use to hash and decode
  4. Create controllers that handles the basic setup you have above

For example, if you have the following configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/home","/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") // this is necessary for a customized login error page and logout page as well
.loginProcessingUrl("/login") // this has to be the same as the above
.defaultSuccessUrl("/", true) // redirect after success
.failureUrl("/login?error=true") // redirect after failed, adds information for processing
.and()
.logout()
.logoutUrl("/logout") // customized logout page
.logoutSuccessUrl("/login?logout=true") // redirect after success
.permitAll()
.and()
.httpBasic();
}

@Bean
public UserDetailsService userDetailsService(){
return new MyUserDetailService();
}

@Bean
public PasswordEncoder bCryptPasswordEncoder(PasswordStorage passwordStorage){
return passwordStorage.getPasswordEncoder();
}
}

The above setup will:

  • allow all access to /, /home, /login* URLs
  • requires a csrf token when the form is submitted
    • this will be enabled unless you specify csrf().disabled()
  • allows a custom login page,login success redirect page, login error page, logout page, logout success redirect page
    • Notice, if you want to specify customized page for all three, you need to specify .loginPage(). If you did not specify this, all the redirecting will not work properly either
  • Injects the two necessary bean into the container to customize the behavior: UserDetailService and PasswordEncoder

Now, your loadUserByUsername implementation could look like:

1
2
3
4
5
6
7
8
9
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserServiceImpl userService;

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return new UserDetailsImpl(s, this.userService);
}
}

And your corresponding UserDetailsImpl could be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.example.testspringsecurity.dto;

import com.example.testspringsecurity.mapper.UserMapper;
import com.example.testspringsecurity.service.UserServiceImpl;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.util.Collection;

public class UserDetailsImpl implements UserDetails {
private UserServiceImpl userService;
private String username;

public UserDetailsImpl(String username, UserServiceImpl userService) {
this.username = username;
this.userService = userService;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null; // this will be used when we come to Authorization with Spring Security
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

@Override
public String getPassword() {
return this.userService.getUserPassword(this.username);
}

@Override
public String getUsername() {
return this.username;
}
}

Since the data comes from a database, you need to have the corresponding service classes. Below shows one of them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class UserServiceImpl {
private UserMapper userMapper;

// so that if this method gets called, userMapper is already initialized
public UserServiceImpl(UserMapper userMapper){
this.userMapper = userMapper;
}

public UserMapper getUserMapper() {
return userMapper;
}

public String getUserPassword(String username){
String passwd;
if((passwd = getUserMapper().getPassword(username)) == null){
throw new UsernameNotFoundException("User "+username+" not found"); // this is needed so that error page redirects correctly
}else{
return passwd;
}
}
}

Notice that you need to use the UsernameNotFoundException for the default login error handler to respond correctly.

Last but not least, if you have enabled the csrf , you need to make sure that the token is included in your login/logout requests.

Luckily, this token is exposed by HttpServletRequest as a _csrf attribute. So I could get this and renders it together within the template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Controller // notice that you need to change this to Controller, not RestController
public class Login {
@Autowired
PasswordStorage passwordStorage;
@Autowired
private HttpServletRequest request;

@GetMapping("/login")
public String login(@RequestParam(required = false) String error, @RequestParam(required = false) String logout, Map<String, String> map1){
map1.put("_csrf",((CsrfToken)this.request.getAttribute("_csrf")).getToken()); // gets the CSRF token

if(error != null){
map1.put("login_status","Authentication Failed"); // this is used for thymeleaf rendering
System.out.println("Authentication Failed");
}

if(logout != null){
map1.put("logout_status","Logout success"); // this is used for thymeleaf rendering
System.out.println("Logged out");
}
return "login";
}

@GetMapping("/logout")
public String logout(Map<String, String> map1){
map1.put("_csrf",((CsrfToken)this.request.getAttribute("_csrf")).getToken());
return "logout";
}
}

Here Thymeleaf is used to silently include the csrf token. A sample login page html could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<html xmlns:th="http://www.w3.org/1999/xhtml">
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Login Page</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<h1>Login</h1>
<div th:if="${login_status != null}">
<p th:text="${login_status}"></p>
</div>

<div th:if="${logout_status != null}">
<p th:text="${logout_status}"></p>
</div>
<form name='f' action="login" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username' value=''></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<tr>
<td><input name="submit" type="submit" value="submit" /></td>
</tr>
</table>
<!-- Notice this input field -->
<input name="_csrf" type="hidden" th:value="${_csrf}">
</form>
</body>
</html>

As a result, requests will be send with a _csrf token.

Note:

  • For the default configuration, both login and logout only supports a POST method. Therefore, you need to either include the token in the header or in the request body.

Authentication Mechanism

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/authentication/architecture/securitycontextholder.png


In summary, Spring Security uses SecurityContextHolder that holds information about Authentication:

  1. You can get the context with

    1
    2
    SecurityContext context = SecurityContextHolder.getContext();
    Authentication authentication = context.getAuthentication();
  2. The Authentication contains:

    • principal - identifies the user. When authenticating with a username/password this is often an instance of UserDetails.
    • credentials - Often a password. In many cases this will be cleared after the user is authenticated to ensure it is not leaked.
    • authorities - the GrantedAuthoritys are high level permissions the user is granted. A few examples are roles or scopes.
  3. Authentication process happens with AuthenticationManager which has a common implementation called ProviderManager


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 Filterss) that invoked the AuthenticationManager. If you are not integrating with Spring Security’s Filterss 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.

  • In one sentence, this holds/manages multiple AuthenticationProviders, which actually authenticates your users

ProviderManager

ProviderManager is the most commonly used implementation of AuthenticationManager.

ProviderManager delegates to a List of AuthenticationProviders. 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 AuthenticationProviders can authenticate, then authentication will fail with a ProviderNotFoundException which is a special AuthenticationException that indicates the ProviderManager was not configured support the type of Authentication that was passed into it.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/authentication/architecture/providermanager.png

  • In one sentence, you can imagine that this holds a list of authentication implementations (AuthenticationProvider) that passes on and authenticating the user’s information from top to bottom.

AuthenticationProvider

Multiple AuthenticationProviders 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.

  • In one sentence, this is where the actual authentication takes place

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.

  • In one sentence, this is the entry point to redirect unauthenticated request to a login page.

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.

  • In one sentence, this is filter that actually triggers AuthenticationEntryPoint if an unauthenticated request happens.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/authentication/architecture/abstractauthenticationprocessingfilter.png

number 1When 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.

number 2 Next, the Authentication is passed into the AuthenticationManager to be authenticated.

number 3 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.

number 4 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 the SecurityContext to the HttpSession.
  • RememberMeServices.loginSuccess is invoked. If remember me is not configured, this is a no-op.
  • ApplicationEventPublisher publishes an InteractiveAuthenticationSuccessEvent.

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:

The below will only discuss the mechanism used by the section Basic Form Authentication. This means only Form Login and UserDetailsService mentioned above will be covered. For more details on the other options, please visit the official website by clicking the hyperlinks above.

Form Authentication Mechanism

Below is a diagram showing what happens in the section Basic Form Authentication.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/authentication/unpwd/loginurlauthenticationentrypoint.png

number 1 First, a user makes an unauthenticated request to the resource /private for which it is not authorized.

number 2 Spring Security’s FilterSecurityInterceptor indicates that the unauthenticated request is Denied by throwing an AccessDeniedException.

number 3 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.

number 4 The browser will then request the log in page that it was redirected to.

number 5 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.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/authentication/unpwd/usernamepasswordauthenticationfilter.png

number 1 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.

number 2 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.

number 3 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.

number 4 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 an InteractiveAuthenticationSuccessEvent.
  • The AuthenticationSuccessHandler is invoked. Typically this is a SimpleUrlAuthenticationSuccessHandler which will redirect to a request saved by ExceptionTranslationFilter when we redirect to the log in page.

Now in the section Basic Form Authentication, we injected the UserDetailService bean into the container. This will do the following:

  • DaoAuthenticationProvider will use that bean to authenticate your user information

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.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/authentication/unpwd/daoauthenticationprovider.png

number 1The authentication Filter from Reading the Username & Password passes a UsernamePasswordAuthenticationToken to the AuthenticationManager which is implemented by ProviderManager.

number 2 The ProviderManager is configured to use an AuthenticationProvider of type DaoAuthenticationProvider.

number 3 DaoAuthenticationProvider looks up the UserDetails from the UserDetailsService.

number 4 DaoAuthenticationProvider then uses the PasswordEncoder to validate the password on the UserDetails returned in the previous step.

number 5 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.

UserDetails and UserDetailsService

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.

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 built-in 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

  • In one sentence, UserDetailsService returns a UserDetails that contains user’s correct login information, so that it can be checked against by the DaoAuthenticationProvider

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.

For example, as demonstrated in the section Basic Form Authentication:

1
2
3
4
@Bean
public PasswordEncoder bCryptPasswordEncoder(PasswordStorage passwordStorage){
return passwordStorage.getPasswordEncoder();
}

Authentication with Session

By default, sessions will be created by Spring Security for already authenticated users to access resources without authenticating again.

We can control exactly when our session gets created and how Spring Security will interact with it:

  • always – a session will always be created if one doesn’t already exist
  • ifRequired – a session will be created only if required (default)
  • never – the framework will never create a session itself but it will use one if it already exists
  • stateless – no session will be created or used by Spring Security

Java configuration:

1
2
3
4
5
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
}

It’s very important to understand that this configuration only controls what Spring Security does – not the entire application. Spring Security may not create the session if we instruct it not to, but our app may!

By default, Spring Security will create a session when it needs one – this is “ifRequired“.

For a more stateless application, the “never” option will ensure that Spring Security itself will not create any session; however, if the application creates one, then Spring Security will make use of it.

Finally, the strictest session creation option – “stateless” – is a guarantee that the application will not create any session at all.

Session Creation Mechanism

Before executing the Authentication process, Spring Security will run a filter responsible with storing the Security Context between requests – the SecurityContextPersistenceFilter. The context will be stored according to a strategy – HttpSessionSecurityContextRepository by default – which uses the HTTP Session as storage.

For the strict create-session=”stateless” attribute, this strategy will be replaced with another – NullSecurityContextRepository – and no session will be created or used to keep the context.

Maximum Session

You can control how many active sessions a user can have at a time.

When a user that is already authenticated tries to authenticate again, the application can deal with that event in one of a few ways. It can either invalidate the active session of the user and authenticate the user again with a new session, or allow both sessions to exist concurrently.


In summary, to configure how many concurrent sessions a user can have, you need to:

  1. Inject a bean with HttpSessionEventPublisher
    • This will notify Spring Security session registry when a session is destroyed
  2. Use the configure() method with sessionManagement().maximumSession(<number>)

For example, extending the example from above sections:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/home","/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/", true)
.failureUrl("/login?error=true")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true").deleteCookies("JSESSIONID")
.permitAll()
.and()
.httpBasic()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // also the default
.maximumSessions(1) // allows only 1 session
.maxSessionsPreventsLogin(true)
;
}

@Bean
public UserDetailsService userDetailsService(){
return new MyUserDetailService();
}

@Bean
public PasswordEncoder bCryptPasswordEncoder(PasswordStorage passwordStorage){
return passwordStorage.getPasswordEncoder();
}

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}

Session Timeout

By default, a session is active for 15 minutes. After the session has timed out, if the user sends a request with an expired session id, they will be redirected to a URL configurable.


In summary, all you have to do is similar to the above:

  • Use the configure() method and specify the expiration URL
  • Configure the timeout limit with server.servlet.session.timeout

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/home","/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/", true)
.failureUrl("/login?error=true")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true").deleteCookies("JSESSIONID")
.permitAll()
.and()
.httpBasic()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // also the default
.maximumSessions(1)
.expiredUrl("/login?expired=true") // redirect
.maxSessionsPreventsLogin(true)
;
}

@Bean
public UserDetailsService userDetailsService(){
return new MyUserDetailService();
}

@Bean
public PasswordEncoder bCryptPasswordEncoder(PasswordStorage passwordStorage){
return passwordStorage.getPasswordEncoder();
}

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}

Then, if you want to change the session timeout period, you can do so:

1
2
3
4
server:
servlet:
session:
timeout: 1 # session time out in minute

Note:

  • Since Spring Boot uses Tomcat, we have to keep in mind that it only supports minute precision for session timeout, with a minimum of one minute. This means that if we specify a timeout value of 170s for example, it will result in a 2 minutes timeout.

Session Tracking

When a user gets authenticated, a session with JSESSIONID is created.

You can either store your session inside a cookie, or inside your URL parameter when you submit a request.

In general, it is safer to store it inside a cookie, which also allows additionally security parameters such as:

  • httpOnly: if true then browser script won’t be able to access the cookie
  • secure: if true then the cookie will be sent only over HTTPS connection

In summary, to configure this, you need to:

  1. Obtain the servletContext and call setSessionTrakcingModes()
  2. Specify the properties to configure cookie security options:
    • server.servlet.session.cookie.http-only=true
    • server.servlet.session.cookie.secure=true

For example, to set the session tracking to cookie, you need to:

1
servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.COOKIE));

and then:

1
2
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=false

The above configuration for cookie security is also the default configuration.

Session Fixation

The framework offers protection against typical Session Fixation attacks by configuring what happens to an existing session when the user tries to authenticate again.

By default, Spring Security has this protection enabled (“migrateSession“) – on authentication a new HTTP Session is created, the old one is invalidated and the attributes from the old session are copied over.

If this is not the desired behavior, two other options are available:

  • when “none” is set, the original session will not be invalidated
  • when “newSession” is set, a clean session will be created without any of the attributes from the old session being copied over

In summary, to configure the above is trivial:

  • Use the configure() method and specify the option sessionFixation() with the desired strategy

For example:

1
2
http.sessionManagement()
.sessionFixation().migrateSession()

Remember Me

Remember-me is basically a mechanism to remember login details between browser sessions, namely, refreshing the session as long as the remember-me status has not expired.

In general, there are two ways that remember-me functionality can be easily integrated with Spring Security + Spring Boot:

  1. Using a remember-me cookie
    • This, as you will see in the Remember Me - Cookie section, is very easy to implement as Spring Security has done most of them for you
    • However, this has security issues. Since it is a cookie that the server uses to refresh/regenerate session, a malicious user can use this cookie to access authenticated content easily as well
  2. Using data from a database to store remember-me information
    • This is harder to setup, yet it is a more secure way for using remember-me

In summary, all you need to do is:

  1. Use the configure() method to configure rememberMe()
  2. Add a checkbox with name=remember-me specified
    • The parameter name remember-me can also be configured in the first step

For example, your can have:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.rememberMe()
.key("Test-Key")
.rememberMeParameter("remember-me1")
.tokenValiditySeconds(600); // when this will expire
}

// extending from the code above
...

}

Then your html code could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<form name='f' action="/login" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username' value=''></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<tr>
<td>Remember Me:</td>
<!-- Notice that the parameter here has to match the rememberMeParamter() specified above -->
<td><input type="checkbox" name="remember-me1" /></td>
</tr>
<tr>
<td><input name="submit" type="submit" value="submit" /></td>
</tr>
</table>
<input name="_csrf" type="hidden" th:value="${_csrf}">
</form>

As a result, your login POST request will look like:

1
username=test&password=12345&remember-me1=on&submit=submit&_csrf=72befc82-f007-4890-ac64-0d707d3de1fd

Now, if you logged in correctly, you will get two cookies, the session cookie and the remember-me cookie.

However, the Remember Me cookie contains the following data:

  • username – to identify the logged in principal
  • expirationTime – to expire the cookie; default is 2 weeks
  • MD5 hash – of the previous 2 values – username and expirationTime, plus the password and the predefined key

First thing to notice here is that both the username and the password are part of the cookie – this means that, if either is changed, the cookie is no longer valid. Also, the username can be read from the cookie.

Additionally, it is important to understand that this mechanism is potentially vulnerable if the remember me cookie is captured. The cookie will be valid and usable until it expires or the credentials are changed.

Note:

  • While the above is easy to implement, once the remember-me token gets leaked, other malicious users can easily log-in to your account. If you need one with a stronger security, please see the section below, which essentially uses a database.

Remember Me - Persistence

This seems only possible with XML based configuration, as one key step includes setting:

1
2
3
4
<http>
...
<remember-me data-source-ref="someDataSource"/>
</http>

And I could not find the API in RememberMeConfigurer that configures the data source. I might be wrong at this, but if you are fine with using XML based configurations, please continue with: https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/#remember-me-persistent-token.

Prevent Brute Force Authentication

A brute force authentication include trying each possible username/password combination until a correct one hits.

In this section, such prevention is implemented by keeping a record of the number of failed attempts originating from a single IP address. If that particular IP goes over a set number of requests – it will be blocked for 5 minutes (you can of course customize this).

Note:

  • Other ways include directly blocking that account based on the username. However, this can cause issues such as a user accidentally blocking another user’s account unintentionally. In this section, we choose the approach to block that specific IP address, allowing the actual user (hopefully) being unaffected from such accidents.

In summary, all you need to do is:

  1. Create AuthenticationSuccessListener and AuthenticationFailureListeners to listen for the authentication event
    • The two listeners of course implements their corresponding event listener interfaces. Check the examples below.
  2. Create a LoginAttemptService, which connects to your redis to read/update/delete login attemps information based on IP address
  3. Update the isAccountNonBlocked() method in your UserDetailsImpl class based on the login attemps

For example, your two listeners look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class AuthenticationFailureListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent>{
@Autowired
private LoginAttemptService loginAttemptService;

/**
*@param authenticationFailureBadCredentialsEvent used to fetch the IP address
*/
@Override
public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent authenticationFailureBadCredentialsEvent) {
WebAuthenticationDetails auth = (WebAuthenticationDetails)authenticationFailureBadCredentialsEvent.getAuthentication().getDetails();
loginAttemptService.loginFailed(auth.getRemoteAddress());
}
}

and

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class AuthenticationSuccessListener implements ApplicationListener<AuthenticationSuccessEvent> {

@Autowired
private LoginAttemptService loginAttemptService;

public void onApplicationEvent(AuthenticationSuccessEvent e) {
WebAuthenticationDetails auth = (WebAuthenticationDetails)
e.getAuthentication().getDetails();

loginAttemptService.loginSucceeded(auth.getRemoteAddress());
}
}

The above two will be executed once the corresponding event is captured, and they evoke the corresponding methods in LoginAttemptService.

Now, you need to implement your loginAttemptService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
* Used to provide API checking current login attempts and block status
*/
@Service
public class LoginAttemptService {
private final int MAX_ATTEMPT = 5;
@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
public LoginAttemptService(){
}

/**
* Deletes the redis entry if login is successful (redis returns 0 even if the key does not exist)
* @param address Client's IP address
*/
public void loginSucceeded(String address) {
redisTemplate.opsForValue().getOperations().delete(address);
}

/**
* If login fails, increase the number of attempts tried
* If it is the MAX_ATTEMPT number of times, sets the expiration time
* @param address Client's IP address
*/
public void loginFailed(String address) {
redisTemplate.opsForValue().increment(address);
if(Integer.parseInt(redisTemplate.opsForValue().get(address)) == MAX_ATTEMPT) {
if (redisTemplate.opsForValue().getOperations().getExpire(address) < 0) {
redisTemplate.opsForValue().getOperations().expire(address, 5, TimeUnit.MINUTES);
}
}
}

/**
* Check if a specific address is blocked for logins. This is configured to block for 5 minutes
* Try block is necessary because, the Authentication procedure is:
* 1. Look at isBlocked from UserDetails
* 1. If blocked, then login fails
* 2. If not, proceed
* 2. Proceed to check login credentials
* @param address the client's IP address
* @return whether if the IP address is blocked for all logins
*/
public boolean isBlocked(String address) {
try{
if(Integer.parseInt(redisTemplate.opsForValue().get(address)) >= MAX_ATTEMPT){
return true;
}
return false;
}catch(NumberFormatException e){
return false;
}
}

/**
* This could be useful for frontend to know
* @param address client's IP address
* @return Number of attempts left
*/
public int remainingAttempts(String address){
try{
int attempts = Integer.parseInt(redisTemplate.opsForValue().get(address));
return MAX_ATTEMPT - attempts;
}catch(NumberFormatException e){
return MAX_ATTEMPT;
}
}
}

Of course, the above assumes a redis connection configured in your application.yml.

Lastly, you can update your UserDetailsImpl to reflect the blocking status:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class UserDetailsImpl implements UserDetails {
private UserServiceImpl userService;
private String username;
HttpServletRequest request;
LoginAttemptService loginAttemptService;

// this is to prevent NullPointerExceptions
public UserDetailsImpl(String username, UserServiceImpl userService, LoginAttemptService loginAttemptService, HttpServletRequest request) {
this.username = username;
this.userService = userService;
this.loginAttemptService = loginAttemptService;
this.request = request;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

/**
* Determines whether if account is locked by wrong attempts
* @return true if attempt < 5; false if attempt >= 5
*/
@Override
public boolean isAccountNonLocked() {
String ip = request.getRemoteAddr();
if (loginAttemptService.isBlocked(ip)) {
return false;
}
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

@Override
public String getPassword() {
return this.userService.getUserPassword(this.username);
}

@Override
public String getUsername() {
return this.username;
}
}

Authentication with Token

:)

Authorization Realizations

While authentication means making sure whether if a user is a legally registered user in your application, authorization deals with granting that legally registered user the ability to access certain resources/services in your application.

Some common authorization model includes Role Based Authorization Control and Resource Based Authorization Control. While Role Based Authorization Control might be more straightforward to think of in real life (e.g. a student can visit student residential halls, while a teacher can visit both student and teacher’s residential halls), which filters authorization based on who is accessing, it is generally easier to implement access control with Resource Based Authorization Control, which filters authorization based on what is being accessed.

The following sections all cover the approach of using Resource Based Authorization Control.

Basic Authorization using SimpleGrantedAuthority


In summary, you need to:

  1. Add an additional roles column in your database for storing user informations
    • You can also use a separate table, if you want
  2. Create/update your corresponding database mappers, UserDetailsImpl, and UserDetailsService
    • Specifically, implement the getAuthorities() method in the UserDetailsImpl
  3. Update your config() method with antMatcher().hasAuthority() to specify which resource is available to which authority

Note:

  • In the text above (and in the following sections), you can assume that role has a synonymous meaning as an authority, i.e. a role=admin has a corresponding ROLE_ADMIN authority.

For example, your updated database should look like:

1
2
3
id	name	password	roles
1 test {bcrypt}$2a$10$WI2BDuq7V2fzEEUhOasn.2qXsfmkDMiV7f1XVmG1ZToZdilkR7Z8G admin;user
2 wkkk {bcrypt}$2a$10$gHDDT.CVYbA9TqvlLA7TZuZGm1RrxeyVeNaby7x8KHHOGx/N3efHm user

and your mechanism for fetching the roles from the database could be:

1
2
3
4
5
6
7
8
9
10
11
12
public List<SimpleGrantedAuthority> getAuthorities(String username){
String roles;
// the mapper implementation is trivial, so it is not shown here
if((roles = getUserMapper().getRoles(username)) == null){
throw new UsernameNotFoundException("User "+username+" not found");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for(String role: roles.split(";")){ // you can change this seperator of course
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}

Now, importantly, you update your UserDetailsImpl class, as this is the class that Spring Security uses to get data about a user:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.example.testspringsecurity.dto;

import com.example.testspringsecurity.service.LoginAttemptService;
import com.example.testspringsecurity.service.UserServiceImpl;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.security.auth.login.AccountLockedException;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.Date;

public class UserDetailsImpl implements UserDetails {
private UserServiceImpl userService;
private String username;
HttpServletRequest request;
LoginAttemptService loginAttemptService;

public UserDetailsImpl(String username, UserServiceImpl userService, LoginAttemptService loginAttemptService, HttpServletRequest request) {
this.username = username;
this.userService = userService;
this.loginAttemptService = loginAttemptService;
this.request = request;
}

// gets the authorities by username
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.userService.getAuthorities(this.username);
}

@Override
public boolean isAccountNonExpired() {
return true;
}

/**
* Determines whether if account is locked by wrong attempts
* @return true if attempt < 5; false if attempt >= 5
*/
@Override
public boolean isAccountNonLocked() {
String ip = request.getRemoteAddr();
if (loginAttemptService.isBlocked(ip)) {
return false;
}
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

@Override
public String getPassword() {
return this.userService.getUserPassword(this.username);
}

@Override
public String getUsername() {
return this.username;
}
}

Lastly, you customize which resource is limited to which role within the configure() method:

1
2
3
4
5
6
7
8
9
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/home","/login*").permitAll()
.antMatchers("/api/*").hasAuthority("admin") // if you use hasRole(), it will automatically insert ROLE_ prefix
.antMatchers("/resource*").hasAuthority("user")
.anyRequest().authenticated()
// other configurations
}

Authorization Mechanism

Authority/GrantedAuthority

Authentication, discusses how all Authentication implementations store a list of GrantedAuthority objects. These represent the authorities that have been granted to the principal. Those 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:

1
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.

  • In one sentence, authorities are stored in objects implementing GrantedAuthority, and an AccessDecisionManager decide, by reading from the GrantedAuthority, whether a request can be accessed by a user.

Pre-Invocation Handling

In short, Spring Security deals with access decision by having pre-invocation handling mechanism (determines whether if a resource can be accessed) and a post-invocation handling mechanism (whether if and how a resource should be altered before giving to the user).

AccessDecisionManager

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 is called by the AbstractSecurityInterceptor and is responsible for making final access control decisions. The AccessDecisionManager interface contains three methods:

1
2
3
4
5
6
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.

  • In one sentence, this is where the access decision actually takes place.
Voting-Based AccessDecisionManager

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.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/access-decision-voting.png

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:

1
2
3
4
5
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 (AccessDecisionManager) 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 behavior 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.

  • In one sentence, some Spring Security implemented AccessDecisionManager uses a voting system, which tally integer vote results from a group of Voters, and decide whether if to throw AccessDeniedException.
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_ (see code snippet below). 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.

1
2
3
4
5
6
7
8
9
10
11
public class RoleVoter implements AccessDecisionVoter<Object> {
private String rolePrefix = "ROLE_";

public RoleVoter() {
}

public String getRolePrefix() {
return this.rolePrefix;
}
...
}
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.

Post-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.

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/after-invocation.png

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:

  • setting “allowIfAllAbstainDecisions” to true (although this is generally not recommended) or
  • 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.

Overall Work Flow

Image from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/images/servlet/authorization/filtersecurityinterceptor.png

  • number 1 First, the FilterSecurityInterceptor obtains an Authentication from the SecurityContextHolder.
  • number 2 Second, FilterSecurityInterceptor creates a FilterInvocation from the HttpServletRequest, HttpServletResponse, and FilterChain that are passed into the FilterSecurityInterceptor.
  • number 3 Next, it passes the FilterInvocation to SecurityMetadataSource to get the ConfigAttributes.
  • number 4 Finally, it passes the Authentication, FilterInvocation, and ConfigAttributes to the AccessDecisionManager.
    • number 5 If authorization is denied, an AccessDeniedException is thrown. In this case the ExceptionTranslationFilter handles the AccessDeniedException.
    • number 6 If access is granted, FilterSecurityInterceptor continues with the FilterChain which allows the application to process normally.

For example:

1
2
3
4
5
6
7
8
9
10
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize
.mvcMatchers("/resources/**", "/signup", "/about").permitAll()
.mvcMatchers("/admin/**").hasRole("ADMIN")
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().denyAll()
);
}

where

  • 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.

Expression-Based Access Control

In short, expression-based access control is built on the same architecture mentioned above but allows complicated Boolean logic to be encapsulated in a single expression.

Common Built-in Expressions

Some of the common expressions used are:

Expression Description
hasAuthority(String authority) Returns true if the current principal has the specified authority. For example, hasAuthority('admin')
hasAnyAuthority(String… authorities) Returns true if the current principal has any of the supplied authorities (given as a comma-separated list of strings)For example, hasAnyAuthority('admin', 'user')
principal Allows direct access to the principal object representing the current user
authentication Allows direct access to the current Authentication object obtained from the SecurityContext
permitAll Always evaluates to true
denyAll Always evaluates to false
isAnonymous() Returns true if the current principal is an anonymous user
isRememberMe() Returns true if the current principal is a remember-me user
isAuthenticated() Returns true if the user is not anonymous
isFullyAuthenticated() Returns true if the user is not an anonymous or a remember-me user
hasPermission(Object target, Object permission) Returns true if the user has access to the provided target for the given permission. For example, hasPermission(domainObject, 'read')
hasPermission(Object targetId, String targetType, Object permission) Returns true if the user has access to the provided target for the given permission. For example, hasPermission(1, 'com.example.domain.Message', 'read')

Table from https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/#el-common-built-in

An example usage would be:

1
2
3
4
5
6
7
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/home","/login*").permitAll()
.antMatchers("/api/*").access("isFullyAuthenticated() and hasAuthority('admin')")
.anyRequest().authenticated();
}

where:

  • any request to /api/* will need an admin that is fully authenticated (not from a remember-me cookie)

Referring to Custom Method in Web Security Expressions

If you want to customize your access logic entirely, you can do so as well!


In summary, this is achieved by:

  1. Creating a bean class, which contains a method that returns a boolean, determining whether if an access is permitted or not
    • If that class is not injected as a bean, the SpEL cannot find that class in the expression
  2. Refer to that method with @<beanName>.<methodName()>

For example, you can have:

1
2
3
4
5
6
7
8
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/home","/login*").permitAll()
.antMatchers("/api/resource1*").access("isFullyAuthenticated() and hasAuthority('admin')")
.antMatchers("/api/resource2").access("@myWebSecurity.checkResource2()") // notice here
.anyRequest().authenticated()
}

Then this is the checkResouce2() method inside the MyWebSecurity class

Note:

  • If the class has name MyWebSecurity, then the bean has name myWebSecurity, with the first letter being lower case.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
public class MyWebSecurity {
@Autowired
private HttpServletRequest request;
@Autowired
private UserServiceImpl userService;

public boolean checkResource2(){
Authentication authentication = this.getAuthentication();
boolean test = this.request.isRequestedSessionIdFromCookie();
for(GrantedAuthority authority: authentication.getAuthorities()){
if(authority.getAuthority().equals("admin")){ // only allows admins
try{
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
if(this.userService.getId(userDetails.getUsername()) == 1){ // only allows admin with id=1
return true;
}
return false;
}catch(Exception e){
return false;
}
}
}
return false;
}

private Authentication getAuthentication(){
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
return authentication;
}
}

The above is just a simple example, yet it demonstrates the potential of customizing complex logics for access evaluation as well.

Using Path Variable

Sometimes, it might be useful to retrieve parts of the URL to include in your access logic.

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:

1
2
3
4
5
http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(#userId)")
...
);

Then all you need to do is to change your checkUserId() method inside your WebSecurity class to take the input userId.

Method Based Security Expressions

While the above authorizes access based on URL, you can also configure access rights based on which methods will be executed.

In general, Spring Security has:

  • @PreAuthorize
  • @PostAuthorize
  • @PreFilter
  • @PostFilter

To use those annotations, you need to:

  1. Add the annotation:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    @EnableWebSecurity
    // enable the annotations listed above
    @EnableGlobalMethodSecurity(
    prePostEnabled = true
    )
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    }
  2. Make sure the condition does not collide with your config() method inside the class above (in which the access logics specified in the config() will take precedence)

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)

1
2
@PreAuthorize("hasRole('USER')")
public void 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:

1
2
@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, 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. In this way, you can access any of the method arguments by name as expression variables.

Note:

  • The built-in hasPermission() expression is linked into the Spring Security ACL module through the application context, as we’ll in the section The PermissionEvaluator interface/hasPermission()

  • Those annotation will not work if you have already specified access rules with the config() method. For example, if a request has passed through the conditions you listed:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    protected void configure(HttpSecurity http) throws Exception {
    http
    .authorizeRequests()
    .antMatchers("/","/home","/login*").permitAll()
    .antMatchers("/api/resource1*").access("isFullyAuthenticated() and hasAuthority('admin')")
    .antMatchers("/api/resource2").access("@myWebSecurity.checkResource2()")
    .anyRequest().authenticated()
    ...
    }

    And you used a @PreAuthorize() on a request that has already passed through, it will execute regardless of your @PreAuthorize condition.

As the actual parameter is being automatically resolved by Spring Security, you can also designate those parameters to reduce accidental conflicts:

  • You can use the Spring Security @P annotation on an argument

    • For example:

      1
      2
      @PreAuthorize("#c.name == authentication.name")
      public void doSomething(@P("c") Contact contact);
  • You can also use the Spring Data @Param annotation on an argument

    • For example:

      1
      2
      @PreAuthorize("#n == authentication.name")
      Contact findContactByName(@Param("n") String name);

Less commonly, you may wish to perform an access-control check after the method has been invoked. This can be achieved using the @PostAuthorize annotation. To access the return value from a method, use the built-in name returnObject in the expression.

Note:

  • If the condition evaluated to false in the @PreAuthorize(), you will get a AccessDeniedException thrown, which you have to handle by yourself.
Using @PreFilter and @PostFilter

As you may already be aware, Spring Security supports filtering of collections and arrays and this can now be achieved using expressions. This is most commonly performed on the return value of a method. For example:

1
2
3
@PreAuthorize("hasAuthority('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();

where:

  • The name filterObject refers to the current object in the collection.

When using the @PostFilter annotation, Spring Security iterates through the returned collection and removes any elements for which the supplied expression is false.

Technically, you can also filter before the method call (i.e. filtering the arguments), 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.

  • For example:

    1
    2
    3
    4
    5
    @PreFilter(value = "filterObject != authentication.principal.username",
    filterTarget = "usernames")
    public String joinUsernamesAndRoles(List<String> usernames, List<String> roles) {
    ...
    }

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.

The PermissionEvaluator interface/hasPermission()

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:

1
2
3
4
5
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:

1
2
3
4
5
6
@Bean
public DefaultMethodSecurityExpressionHandler expressionHandler(PermissionEvaluator myPermissionEvaluator){
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(myPermissionEvaluator);
return handler;
}

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.

Using @Secure

This is basically a shortcut of using @PreAuthorize(), if the conditions are simple.

Alike using @PreAuthorize()/@PreFilter/…, you need to enable the global method security with:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
prePostEnabled = true, // enables @PreAuthorize/... etc
securedEnabled = true // enables @Secure
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
}

Then, you can use the @Secure annotation to a method (on a class or interface), which 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:

1
2
3
4
5
6
7
8
9
10
11
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);
}

and the equivalent @PreAuthorize() version would be

1
2
3
4
5
6
7
8
9
10
11
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);
}

However, as you have guessed, the @Secured annotation does not support SpEL expressions.

Domain Object Security (ACLs)

The above access control strategies are straightforward and simple. However, as you might have noticed, if there are more complex access control needs, where security decisions need to comprise both who (Authentication), where (MethodInvocation), and what (SomeDomainObject), then implementing just using the techniques above would be costly.

While solving such needs can be achieved by using the ACLs, it is a topic more related to controlling access rights of large scale businesses. Since this manual aims to cover the basics instead, please visit the official documentation https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/#domain-acls for more information on this topic.

Registration Realizations

Now we have covered authentication (login) and authorization (access control). Registration is another important feature for user interaction, and, in fact, its basic implementation is pretty much the same as the Basic Form Authorization, in which you need to get data from a POST request, and after some security checks, insert the data into your database.

However, there are some additional techniques for registration, which we will also cover in the following section:

  • account activation with email
  • resend verification email
  • resetting password

Basic Form Registration

Alike other sections, we will first cover the basic registration technique with a form. We will also cover some form input verification techniques here.


In summary, we need to:

  1. Create a UserDto (User Data Transfer Object), to encapsulate the information we need from the user
  2. Create your html template, that contains a form with the necessary input fields
    • Note that for the data binding to occur successfully, you need to specify the name=<fieldNameInUserDto> attribute
  3. Create your controller class, to return the registration page for GET request, and to get the data for the POST request
  4. Create your validation logic for the user input
    • This can be done various ways. The most straightforward yet cumbersome way would be programming it directly in your code, which is not recommended.
    • The second way would be creating custom validation annotations, which is a clean and scalable approach. This will be covered here.
  5. Once the information gets validated, implement your corresponding mapper and service classes to insert the data into your database
    • At this point, since it is a simple form registration, we do not (yet) implement account activation with email. This will be covered in the next section.

First, your UserDto object could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class UserDto {
private String username;
private String password;
private String matchingPassword;
private String email;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getMatchingPassword() {
return matchingPassword;
}

public void setMatchingPassword(String matchingPassword) {
this.matchingPassword = matchingPassword;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}
}

where we see that four input fields - username,email, password, matchingPassword - will be needed.

Therefore, our corresponding html page would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<html xmlns:th="http://www.w3.org/1999/xhtml">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>form</h1>
<!-- the POST request will be sent to /register -->
<form action="/register" method="POST" enctype="utf8">
<!-- Notice that the name field has to match the field name in the UserDto for automatica data binding -->
<div>
<label>username</label>
<input name="username"/>
</div>
<div>
<label>email</label>
<input type="email" name="email"/>
</div>
<div>
<label>password</label>
<input type="password" name="password"/>
</div>
<div>
<label>confirm</label>
<input type="password" name="matchingPassword"/>
</div>
<!-- I am afraid that all post request will need this -->
<input name="_csrf" type="hidden" th:value="${_csrf}">
<button type="submit">submit</button>
</form>

<a th:href="@{/login}">login</a>
</body>
</html>

Notice that:

  • Even if there is no formRegister() in the configure() method for Spring Security filters, all POST request, regardless where it comes from, will need to have a _csrf token (unless you do csrf().disable(), which is not recommended). Therefore, in order for this request to be processed correctly, you need to include the _csrf token.

Therefore, your controller class would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
public class Register {
@Autowired
HttpServletRequest request;
@Autowired
UserServiceImpl userService;

@GetMapping("/register")
public String showRegistrationForm(Map<String, String> map1) {
// puts in the _csrf token
map1.put("_csrf",((CsrfToken)this.request.getAttribute("_csrf")).getToken());
return "register";
}

@PostMapping("/register")
@ResponseBody
public String registerUserAccount(@ModelAttribute UserDto userDto, BindingResult bindingResult){
// at this point, we have not implemented the information validations yet
userService.createUser(userDto);
return "registration completed";
}

}

Notice:

  • We did not use @RequestBody, but @ModelAttribute annotation for data binding the UserDto. This is because (in my setup), using @RequestBody results in an error of MediaType not supported. This is because POST request sent data in the form of FORM_URLENCODED_VALUE, which is accepted if you use @ModelAttribute for binding, but not recognized if you use @RequestBody, which takes application/json format.
  • The UserServiceImpl would contain the implementation to encode the password (don’t forget to do this!), and then send data to the database. This implementation is not shown here since it is trivial.

At this point, your registration workflow is already in shape (without input information checks). If you put some data in your html and press submit, data should be sent to your database and a new user would be created.

However, often some sort of validation will be needed to make sure that the user data looks legit before sending and inserting into your database. To do this, we can create our custom validation annotations (quick guides for how to create your custom annotations and how they work can be found at https://dzone.com/articles/creating-custom-annotations-in-java and https://www.baeldung.com/spring-mvc-custom-validator).

First, you will need to import necessary dependencies, including:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>

<!-- These are also necessary somehow for Validators to work -->
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator-annotation-processor -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>6.1.5.Final</version>
</dependency>

Yes, dependencies from hibernate are necessary as well.

Now, you can create your own annotation interface:

1
2
3
4
5
6
7
8
9
10
@Target(ElementType.FIELD)	// specified where this annotation can be applied
@Retention(RetentionPolicy.RUNTIME) // when it will be enacted
@Constraint(validatedBy = CustEmailValidator.class) // how it will validate the target (a field in this case)
public @interface CustEmailValidation {
String message() default "Invalid email"; // this is the most common method to have. It means that if there is an error, your error message can be get from the method message().

// these are here just to comply with the standard, most of the time they will not be used
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

Since we specified @Constraint(validatedBy = CustEmailValidator.class), we need to of course implement the validation logic with another class CustEmailValidator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CustEmailValidator implements ConstraintValidator<CustEmailValidation, String> {
// your validation logic here
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
// our logic here
return false;
}

// some initialization logic on when this wrapper object is created
@Override
public void initialize(CustEmailValidation constraintAnnotation) {

}
}

where:

  • You can imagine this annotations as wrappers around your target: they wrap around your target to access its data, so that they could verify it

  • You need to implement the ConstraintValidator<AnnotationName, EnactedFieldType>

    • Since this annotation is called CustEmailValidator, the AnnotationName is CustEmailValidator
    • Since the target you will wrap around will be String email, which is of type String, your EnactedFieldType will be String
  • The isValid() method will have the first argument a reference to your target object, in this case, String s will hold the data of the String email. Of course it has to be like this so that we could use this data to verify if it is legit.

Now, you could apply your custom annotation to your UserDto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class UserDto {
private String username;
private String password;
private String matchingPassword;
// @CustEmailValidation("Hello") will make the message() method return "Hello" instead of "Invalid Email"
@CustEmailValidation
private String email;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getMatchingPassword() {
return matchingPassword;
}

public void setMatchingPassword(String matchingPassword) {
this.matchingPassword = matchingPassword;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}
}

The above verifies the email field, you should also apply verification on other fields if needed, as well as implementing a way of checking whether if the email address has already been taken :).

And, finally, to fetch the error messages if an error occur, you can do:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@PostMapping("/register")
@ResponseBody
public String registerUserAccount(@Valid UserDto userDto, BindingResult bindingResult){
/**
* One way to get the errors
*/
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<UserDto>> violations = validator.validate(userDto);
for (ConstraintViolation<UserDto> violation : violations) {
System.out.println(violation.getMessage());
}

/**
* The second way
*/
if(bindingResult.hasErrors()){
for(ObjectError error: bindingResult.getAllErrors()){
System.out.println(error.getDefaultMessage());
}
}else{
userService.createUser(userDto);
}

return "registration completed";
}

Account Verification with Email

Again, the idea is simple: once the user submitted the registration form above, you need to generate a unique UUID and that registered username pair to be stored somewhere. Then, you can send an email to the user’s registered email with someURL/confirmRegistration?token=<thatUUID>. Lastly, you create a controller to map to that request, extract the UUID: if that UUID exists in your storage, you set the enabled status of the user to true (in the below implementation, it sets the SQL enabled entry to 1), else, return "link invalid" or equivalent.

Now, detailed implementations for the above may vary, and my implementation will be:


In summary, you will need to:

  1. Create a custom ApplicationEvent (I called it OnRegistrationCompleteEvent) once the user has submitted the form successfully
    • Another way to do this would be directly program the implementation of setting up the UUID and etc. in a method directly evoked by the controller. While this would also work, the way above shows you another common approach and teaches you how to publish/catch custom events.
  2. Create a custom ApplicationListener that listens to the above event you published. In that listener, you configure your UUID and username pair generation/storage and email sending mechanism
  3. Create a class that stores the UUID and username to activate pair, such as MyVerificationToken
  4. Update your controller that maps to POST request for registration, so that it includes the ability to initialize the token and publish the event you created in step 1
  5. Update your database table to have the field enabled, to indicate whether if the account has been activated. You should also update your UserDetailsService and UserDetailsImpl to reflect the enabled field status
  6. Create corresponding service classes, to set/update the enabled field for a user
  7. Create corresponding service classes, to store and look up UUID and username pair in that MyVerficationToken
    • Here I used redis for storing the pair. It is convenient to me because I can also setup an expiration time easily
  8. Create your GET controller for that confirmRegistration?token= link, which uses the above services above to check and update the enabled status of a user
    • Don’t forget to configure your Spring Security to allow unauthenticated users to access this URL!
  9. OPTIONALLY, you can also setup a listener for the built-in AuthenticationFailureDisabledEvent, which will be published if a user tries to login to an account that has not been activated. This is briefly shown in the end, and you can of course customize your code on top of that.

For example, first you create your custom ApplicationEvent object to store the necessary information you will later need to process:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Custom event, which will be sent by event published as an object
* Information contained in this event object can be used to process later on
*/
public class OnRegistrationCompleteEvent extends ApplicationEvent {
private String username;
private Locale locale;
private String appURL;

public OnRegistrationCompleteEvent(MyVerificationToken verificationToken, Locale locale, String appURL) {
super(verificationToken); // whatever I set here can be fetched by getSource()
this.username = verificationToken.getUsername();
this.locale = locale;
this.appURL = appURL;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public Locale getLocale() {
return locale;
}

public void setLocale(Locale locale) {
this.locale = locale;
}

public String getAppURL() {
return appURL;
}

public void setAppURL(String appURL) {
this.appURL = appURL;
}
}

Now, you setup the corresponding listener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class RegistrationCompleteListener implements ApplicationListener<OnRegistrationCompleteEvent> {
@Autowired
UserRegistrationService registrationService;
// @Autowired
// JavaMailSender javaMailSender; // TODO: implement email sending later
@Override
public void onApplicationEvent(OnRegistrationCompleteEvent onRegistrationCompleteEvent) {
this.confirmRegistration(onRegistrationCompleteEvent);
}

// all your resgistration confirmation logic goes here
private void confirmRegistration(OnRegistrationCompleteEvent event){
MyVerificationToken verificationToken = (MyVerificationToken) event.getSource();
registrationService.createUserConfirmation(verificationToken.getUsername(),
verificationToken.getToken(),
MyVerificationToken.getEXPIRATION());

String confirmURL = event.getAppURL() + "/confirmRegistration?token="+verificationToken.getToken();
System.out.println(confirmURL); // TODO: implement email sending here
}
}

where:

  • For the listener to work in the context, you need to inject it into the container with @Component
  • Email sending mechanism is not shown here, but can be setup easily if you follow the guide at: https://www.baeldung.com/spring-email

You see that the above two uses the MyVerificationToken object to fetch the information for the UUID and the username, so below is the MyVerificationToken class (you can see it as a DTO):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyVerificationToken {
private static final int EXPIRATION = 3600;

private String username;
// make sure the token is not changed once created
private final String token;

public MyVerificationToken(String username) {
this.username = username;
this.token = UUID.randomUUID().toString();
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getToken() {
return token;
}

public static int getEXPIRATION() {
return EXPIRATION;
}
}

Now, the basic event handling has been setup, but for it to work, the event has to be published somewhere. And this is done right after the registration has been successful:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Autowired
ApplicationEventPublisher applicationEventPublisher;

@PostMapping("/register")
@ResponseBody // using @RequestBody could throw MediaType not supported error
public String registerUserAccount(@Valid UserDto userDto, BindingResult bindingResult){
if(bindingResult.hasErrors()){
for(ObjectError error: bindingResult.getAllErrors()){
System.out.println(error.getDefaultMessage());
}
}else{ // TODO: add logic to ensure user does not already exist
userService.createUser(userDto);
// publishes the event
applicationEventPublisher.publishEvent(new OnRegistrationCompleteEvent(new MyVerificationToken(userDto.getUsername()),
request.getLocale(), request.getContextPath()));
}

return "registration completed";
}

After that, it is just some clean up for your previous code. For example, you need to also add the field enabled, which I configured it to use TINYINT(1), where 1 would mean true and 0 would be false. I also set it to be 0 by default.

Your updated UserDetailsService could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean isEnabled(String username){
try{
int result = Integer.parseInt(userMapper.getEnabled(username));
if(result == 1){
return true;
}else{
return false;
}
}catch(NumberFormatException e){
return false; // user not found
}
}

public boolean setEnabled(String username){
if(getId(username) == -1){
return false; // user does not exist
}else{
userMapper.updateEnabled(username, 1);
return true;
}
}

and your UserDetailsImpl which is used by Spring Security to check account authentication will look like:

1
2
3
4
@Override
public boolean isEnabled() {
return userService.isEnabled(this.username);
}

If you are interested, below is how I set up (in a simple way) the storage/look up service for the UUID/username pair.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
public class UserRegistrationService {
@Autowired
private StringRedisTemplate redisTemplate;

/**
* Creates a key-value pair in redis to control UUID confirmations
* @param username user to activate
* @param UUID generated UUID for that user
* @param secondsToExpire time for the UUID to expire
* @return creation status
*/
public boolean createUserConfirmation(String username, String UUID, int secondsToExpire){
if(redisTemplate.opsForValue().get(UUID) != null){
return false; // user already registered/email already sent
}else{
redisTemplate.opsForValue().set(UUID, username, secondsToExpire, TimeUnit.SECONDS);
return true;
}
}

/**
* Returns the user to activate for that UUID
* @param UUID the UUID for that user to activate
* @return the username, null if UUID not found
*/
public String activateUser(String UUID){
return redisTemplate.opsForValue().get(UUID);
}
}

Lastly, your controller for confirming the link that is sent could look like:

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/confirmRegistration")
@ResponseBody
public String confirmRegistration(@RequestParam String token){
String username;
if((username = userRegistrationService.activateUser(token)) == null){
return "user activation not found";
}else{
userService.setEnabled(username);
}
return "registration successful";
}

Optionally, the listener for that built-in AuthenticationFailureDisabledEvent would look like:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Used for accounts that are not yet enabled
*/
@Component
public class AccountDisabledListener implements ApplicationListener<AuthenticationFailureDisabledEvent> {

@Override
public void onApplicationEvent(AuthenticationFailureDisabledEvent authenticationFailureDisabledEvent) {
System.out.println("Account disabled");
// your logic here
}
}

OAuth2.0 Realizations

Basically, you need to accomplish two things:

  • obtain OAuth APIs and get necessary user information
  • configure your Spring Security, so that it authenticates users based on OAuth Login
    • this comes in two flavors. For OAuth2.0 API flow that is supported by Spring Security, it can be implemented easily
    • you can also manually implement the authentication flow, which will be discussed in the following

In summary, you need to do the following steps:

  1. Create the class CustomAuthenticationProvider implements AuthenticationProvider, and override the public Authentication authenticate(Authentication authentication) throws AuthenticationException method
  2. Configure Spring Security with configure(AuthenticationManagerBuilder auth), in order to add the above authentication provider into the list of authentication providers used for security checking.
  1. First, you will need to have a class that does the follows:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    @Component
    public class CustomAuthenticationProvider implements AuthenticationProvider { // extends AbstractUserDetailsAuthenticationProvider
    @Autowired
    private HttpServletRequest request;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    String username = (String) authentication.getPrincipal();
    // use the request to get extra parameters you need, otherthan the default username/password
    String verificationType = request.getParameter("verificationType");

    if (shouldAuthenticateAgainstThirdPartySystem(verificationType)) {
    // use the credentials
    // and authenticate against the third-party system
    // ..

    if(successful){
    // returning with Authorities automatically sets authentication to success
    MyUserDetails userDetails = new MyUserDetails(username, userService, null, null);
    return new UsernamePasswordAuthenticationToken(mappedUsername, password, userDetails.getAuthorities());
    }

    throw new UserNotFoundException("Third Party Authentication Failed");
    } else {
    // let the DaoAuthenticationProvider handle it
    // here I have my own version of the DaoAuthenticationProvider. You could also use the Spring provided one.
    DaoAuthenticationProvider provider = new MyDaoAuthProvider();
    provider.setPasswordEncoder(bCryptPasswordEncoder()); // by default, this is null
    provider.setUserDetailsService(userDetailsService()); // by default, this is null
    return provider.authenticate(authentication);
    }
    }

    // this intercepts at the stage of username-password authentication (among the chain of authenticators)
    @Override
    public boolean supports(Class<?> authentication) {
    return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

    private boolean shouldAuthenticateAgainstThirdPartySystem(String verType) throws UserNotFoundException{
    // shouldAuthenticateAgainstThirdParty?

    }
    }

    Notes:

    • basically, the authenticate() method does the job of authenticating the user
  2. Now, to add the above into the list of authenticators, do:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class WebFiltersConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    //..
    }

    /**
    * Use my authentication provider, so that I could intercept for thirdparty user logins
    */
    @Bean
    public CustomAuthenticationProvider authProvider() {
    return new CustomAuthenticationProvider();
    }

    // add the providers into the manager
    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(authProvider());
    }

    // other customized settings
    }

Mechanism Behind the Above Implementation

In short, this is what happens:

  1. Spring Security gets the authentication request, and go to the class ProviderManager to call authenticate.
  2. The above step will get a list of authentication providers, and iterate through them with Iterator var8 = this.getProviders().iterator();, calling authenticate method for each AuthenticationProvider.
    • e.g. the DaoAuthenticationProvider and the CustomAuthenticationProvider you implemented above
  3. If the certain provider returned null, then Spring checks if there is a parent of that AuthenticationProvider.
    • in this case, the CustomAuthenticationProvider is standalone. Hence whatever you returned there will be the final returned result.