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
- Import the
spring-boot-starter-security
in yourpom.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
andOAuth2 Client
yet. If you want, feel free to include those in yourpom.xml
as well.
- You can either manually import the dependency, or select the
The necessary dependency looks like:
1 | <dependencies> |
Again, its version has been already specified in the parent:
1 | <parent> |
If you want to use a specific version, you can specify it as well:
1 | <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.
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 | PasswordEncoder passwordEncoder = |
Alternatively, you may create your own custom instance:
1 | String idForEncode = "bcrypt"; |
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 | spring encodepassword password |
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 | {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG |
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 anIllegalArgumentException
. This behavior can be customized usingDelegatingPasswordEncoder.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 | // Create an encoder with strength 16 |
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 | // Create an encoder with all the defaults |
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 | // Create an encoder with all the defaults |
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 | // Create an encoder with all the defaults |
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 | <form method="post" |
Then the request would look like:
1 | POST /transfer HTTP/1.1 |
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 | <form method="post" |
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:
- The Synchronizer Token Pattern
- Specifying the SameSite Attribute on your session cookie
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
, andTRACE
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:
- When a user logged in, the server should generate a unique CSRF token associated with the user
- 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
- 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 | POST /transfer HTTP/1.1 |
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:
- 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
- For example, if specified
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 toStrict
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 theSameSite
attribute isStrict
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 | Cache-Control: no-cache, no-store, max-age=0, must-revalidate |
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
- Content Type Options
- HTTP Strict Transport Security
- X-Frame-Options
- X-XSS-Protection
- Content-Security-Policy
- Referrer-Policy
- Feature-Policy
- Clear-Site-Data
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 | Cache-Control: no-cache, no-store, max-age=0, must-revalidate |
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
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:
- The
same-origin
policy specifies that- a full URL, stripped for use as a referrer, is sent as referrer information when making same-origin requests from a particular client.
- Cross-origin requests, on the other hand, will contain no referrer information. A
Referer
HTTP header will not be sent.
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 Filter
s
Spring Security’s Servlet support is based on Servlet Filter
s, so it is helpful to look at the role of Filter
s generally first. The picture below shows the typical layering of the handlers for a single HTTP request.
The client sends a request to the application, and the container creates a FilterChain
which contains the Filter
s and Servlet
that should process the HttpServletRequest
based on the path of the request URI.
In a Spring MVC application the
Servlet
is an instance ofDispatcherServlet
.
At most one Servlet
can handle a single HttpServletRequest
and HttpServletResponse
. However, using/having more than one Filter
can be used to:
- Prevent downstream
Filter
s or theServlet
from being invoked. In this instance theFilter
will typically write theHttpServletResponse
. - Modify the
HttpServletRequest
orHttpServletResponse
used by the downstreamFilter
s andServlet
For example:
1 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { |
Since a Filter
only impacts downstream Filter
s 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 Filter
s 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 implementsFilter
.
Here is a picture of how DelegatingFilterProxy
fits into the Filter
s and the FilterChain
.
The pseudo code for DelegatingFilterProxy
could look like:
1 | // inside DelegatingFilterProxy |
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 FilterChainProxy
s, 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.
SecurityFilterChain
SecurityFilterChain
is used by FilterChainProxy
to determine which Spring Security Filter
s should be invoked for this request.
where:
- The Security Filters in
SecurityFilterChain
are also typically Beans (alike Bean Filters), but they are registered withFilterChainProxy
instead of DelegatingFilterProxy.
FilterChainProxy
provides a number of advantages to registering directly with the Servlet container or DelegatingFilterProxy
.
First, it provides a starting point for all of Spring Security’s Servlet support. For that reason, if you are attempting to troubleshoot Spring Security’s Servlet support, adding a debug point in
FilterChainProxy
is a great place to start.Second, since
FilterChainProxy
is central to Spring Security usage it can perform tasks that are not viewed as optional. For example, it clears out theSecurityContext
to avoid memory leaks. It also applies Spring Security’sHttpFirewall
to protect applications against certain types of attacks.In addition, it provides more flexibility in determining when a
SecurityFilterChain
should be invoked. In a Servlet container,Filter
s are invoked based upon the URL alone. However,FilterChainProxy
can determine invocation based upon anything in theHttpServletRequest
by leveraging theRequestMatcher
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.
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 onSecurityFilterChain0
‘s pattern of/api/**
, so onlySecurityFilterChain_0
will be invoked even though it also matches onSecurityFilterChain_n
. If a URL of/messages/
is requested, it will not match onSecurityFilterChain_0
‘s pattern of/api/**
, soFilterChainProxy
will continue trying eachSecurityFilterChain
. Assuming that no other,SecurityFilterChain
instances matchSecurityFilterChain_n
will be invoked.
Security Filters
The Security Filters are inserted into the FilterChainProxy
with the SecurityFilterChain
API. The order of Filter
s matters. It is typically not necessary to know the ordering of Spring Security’s Filter
s. However, there are times that it is beneficial to know the ordering.
Below is a comprehensive list of Spring Security Filter ordering 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
- Does your authorization (see Authorization Realizations)
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
.
where:
- First, the
ExceptionTranslationFilter
invokesFilterChain.doFilter(request, response)
to invoke the rest of the application. - If the user is not authenticated or it is an
AuthenticationException
, then Start Authentication.- The SecurityContextHolder is cleared out
- The
HttpServletRequest
is saved in theRequestCache
. When the user successfully authenticates, theRequestCache
is used to replay the original request. - The
AuthenticationEntryPoint
is used to request credentials from the client. For example, it might redirect to a log in page or send aWWW-Authenticate
header.
- Otherwise if it is an
AccessDeniedException
, then Access Denied. TheAccessDeniedHandler
is invoked to handle access denied.
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 | try { |
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 | <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 namedspringSecurityFilterChain
. This bean is responsible for all the security (protecting the application URLs, validating submitted username and passwords, redirecting to the log in form, and so on) within your application. - Creates a
UserDetailsService
bean with a username ofuser
and a randomly generated password that is logged to the console. - Registers the
Filter
with a bean namedspringSecurityFilterChain
with the Servlet container for every request.
Spring Boot is not configuring much, but it does a lot. A summary of the features follows:
- Require an authenticated user for any interaction with the application
- Generate a default login form for you
- Let the user with a username of
user
and a password that is logged to the console to authenticate with form-based authentication - Protects the password storage with
BCrypt
- Lets the user log out
- CSRF attack prevention
- Session Fixation protection
- Security Header integration
- HTTP Strict Transport Security for secure requests
- X-Content-Type-Options integration
- Cache Control (can be overridden later by your application to allow caching of your static resources)
- X-XSS-Protection integration
- X-Frame-Options integration to help prevent Clickjacking
- Integrate with the following Servlet API methods:
HttpServletRequest#getRemoteUser()
HttpServletRequest.html#getUserPrincipal()
HttpServletRequest.html#isUserInRole(java.lang.String)
- [
HttpServletRequest.html#login(java.lang.String, java.lang.String)
](https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#login(java.lang.String, java.lang.String)) HttpServletRequest.html#logout()
Now, if you start your application and send a request, this would be the response header that Spring Security has configured for you.
1 | HTTP/1.1 200 |
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 iddata
- 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:
- Create all the relevant classes, including your
@Controller
,@Configuration
… - Create a
DTO
, which contains fields such as:username
password
SESSION_ID
- 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 userDTO
as data - Provide the
SESSION_ID
back to be stored in the user’s cookie
- If verified correctly, creates a session with the specified
- Create a
request
object, that contains:- necessary request parameters, such as
username
andpassword
- necessary request parameters, such as
- Create a
controller
, that:- Uses the
request
object created above as input argument - Uses the
service
class you created above for handling authentication verification
- Uses the
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:
- Create a class annotated with
@EnableWebSecurity
- You might want to make it a
@Configuration
as well, as it configures your application
- You might want to make it a
- 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.
- Use the
configure()
method and specify the configurations you want
For example:
1 |
|
where:
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
- for all options you can specify with it, you can look at its class
formLogin()
configures the login request, allowing form login- for all options you can specify with it, you can look at its class
FormLoginConfigurer
- for all options you can specify with it, you can look at its class
logout()
configures the logout request- for all options you can specify with it, you can look at its class
LogoutConfigurer
- for all options you can specify with it, you can look at its class
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 class
HttpBasicConfigurer
- for all options you can specify with it, you can look at its class
In fact, the Spring Security implementation has:
1 | public abstract class WebSecurityConfigurerAdapter implements |
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:
- 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.
- 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.
- 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:
- Use the
config()
method inside a class that extendsWebSecurityConfigurerAdapter
to configure your Spring Security- Shown in the section Configuring your Spring Security above
- Based on what you have configured here, some of your code will change below.
- Implement the
loadUserByUsername
method by creating a class that implementsUserDetailsService
- 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
- At this point, you need to also create a class that implements
- 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
- Create controllers that handles the basic setup you have above
For example, if you have the following configuration:
1 |
|
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()
- this will be enabled unless you specify
- 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
- Notice, if you want to specify customized page for all three, you need to specify
- Injects the two necessary bean into the container to customize the behavior:
UserDetailService
andPasswordEncoder
Now, your loadUserByUsername
implementation could look like:
1 | public class MyUserDetailService implements UserDetailsService { |
And your corresponding UserDetailsImpl
could be:
1 | package com.example.testspringsecurity.dto; |
Since the data comes from a database, you need to have the corresponding service classes. Below shows one of them:
1 |
|
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 | // notice that you need to change this to Controller, not RestController |
Here Thymeleaf
is used to silently include the csrf
token. A sample login page html could look like:
1 | <html xmlns:th="http://www.w3.org/1999/xhtml"> |
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
In summary, Spring Security uses SecurityContextHolder
that holds information about Authentication:
You can get the context with
1
2SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();The
Authentication
contains:principal
- identifies the user. When authenticating with a username/password this is often an instance ofUserDetails
.credentials
- Often a password. In many cases this will be cleared after the user is authenticated to ensure it is not leaked.authorities
- theGrantedAuthority
s are high level permissions the user is granted. A few examples are roles or scopes.
Authentication process happens with
AuthenticationManager
which has a common implementation calledProviderManager
AuthenticationManager
AuthenticationManager
is the API that defines how Spring Security’s Filters perform authentication. The Authentication
that is returned is then set on the SecurityContextHolder
by the controller (i.e. Spring Security’s Filters
s) that invoked the AuthenticationManager
. If you are not integrating with Spring Security’s Filters
s you can set the SecurityContextHolder
directly and are not required to use an AuthenticationManager
.
While the implementation of AuthenticationManager
could be anything, the most common implementation is ProviderManager
.
- 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 AuthenticationProvider
s. Each AuthenticationProvider
has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream AuthenticationProvider
to decide. If none of the configured AuthenticationProvider
s can authenticate, then authentication will fail with a ProviderNotFoundException
which is a special AuthenticationException
that indicates the ProviderManager
was not configured support the type of Authentication
that was passed into it.
- 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 AuthenticationProvider
s can be injected into ProviderManager
. Each AuthenticationProvider
performs a specific type of authentication. For example, DaoAuthenticationProvider
supports username/password based authentication while JwtAuthenticationProvider
supports authenticating a JWT token.
- 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.
When the user submits their credentials, the AbstractAuthenticationProcessingFilter
creates an Authentication
from the HttpServletRequest
to be authenticated. The type of Authentication
created depends on the subclass of AbstractAuthenticationProcessingFilter
. For example, UsernamePasswordAuthenticationFilter
creates a UsernamePasswordAuthenticationToken
from a username and password that are submitted in the HttpServletRequest
.
Next, the Authentication
is passed into the AuthenticationManager
to be authenticated.
If authentication fails, then Failure
- The
SecurityContextHolder
is cleared out. RememberMeServices.loginFail
is invoked. If remember me is not configured, this is a no-op.AuthenticationFailureHandler
is invoked.
If authentication is successful, then Success.
SessionAuthenticationStrategy
is notified of a new log in.- The
Authentication
is set on theSecurityContextHolder
. Later theSecurityContextPersistenceFilter
saves theSecurityContext
to theHttpSession
. RememberMeServices.loginSuccess
is invoked. If remember me is not configured, this is a no-op.ApplicationEventPublisher
publishes anInteractiveAuthenticationSuccessEvent
.
Username/Password Authentication
One of the most common ways to authenticate a user is by validating a username and password. As such, Spring Security provides comprehensive support for authenticating with a username and password.
Reading the Username & Password
Spring Security provides the following built in mechanisms for reading a username and password from the HttpServletRequest
:
Storage Mechanisms
Each of the supported mechanisms for reading a username and password can leverage any of the supported storage mechanisms:
- Simple Storage with In-Memory Authentication
- Relational Databases with JDBC Authentication
- Custom data stores with UserDetailsService
- This is mentioned in the Basic Form Authentication section
- LDAP storage with LDAP Authentication
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.
First, a user makes an unauthenticated request to the resource /private
for which it is not authorized.
Spring Security’s FilterSecurityInterceptor
indicates that the unauthenticated request is Denied by throwing an AccessDeniedException
.
Since the user is not authenticated, ExceptionTranslationFilter
initiates Start Authentication and sends a redirect to the log in page with the configured AuthenticationEntryPoint
. In most cases the AuthenticationEntryPoint
is an instance of LoginUrlAuthenticationEntryPoint
.
The browser will then request the log in page that it was redirected to.
Something within the application, must render the log in page.
When the username and password are submitted, the UsernamePasswordAuthenticationFilter
authenticates the username and password. The UsernamePasswordAuthenticationFilter
extends AbstractAuthenticationProcessingFilter, so this diagram should look pretty similar.
When the user submits their username and password, the UsernamePasswordAuthenticationFilter
creates a UsernamePasswordAuthenticationToken
which is a type of Authentication
by extracting the username and password from the HttpServletRequest
.
Next, the UsernamePasswordAuthenticationToken
is passed into the AuthenticationManager
to be authenticated. The details of what AuthenticationManager
look like depend on how the user information is stored.
If authentication fails, then Failure
- The SecurityContextHolder is cleared out.
RememberMeServices.loginFail
is invoked. If remember me is not configured, this is a no-op.AuthenticationFailureHandler
is invoked.
If authentication is successful, then Success.
SessionAuthenticationStrategy
is notified of a new log in.- The
Authentication
is set on the SecurityContextHolder. RememberMeServices.loginSuccess
is invoked. If remember me is not configured, this is a no-op.ApplicationEventPublisher
publishes anInteractiveAuthenticationSuccessEvent
.- The
AuthenticationSuccessHandler
is invoked. Typically this is aSimpleUrlAuthenticationSuccessHandler
which will redirect to a request saved byExceptionTranslationFilter
when we redirect to the log in page.
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.
The authentication Filter
from Reading the Username & Password passes a UsernamePasswordAuthenticationToken
to the AuthenticationManager
which is implemented by ProviderManager
.
The ProviderManager
is configured to use an AuthenticationProvider of type DaoAuthenticationProvider
.
DaoAuthenticationProvider
looks up the UserDetails
from the UserDetailsService
.
DaoAuthenticationProvider
then uses the PasswordEncoder
to validate the password on the UserDetails
returned in the previous step.
When authentication is successful, the Authentication
that is returned is of type UsernamePasswordAuthenticationToken
and has a principal that is the UserDetails
returned by the configured UserDetailsService
. Ultimately, the returned UsernamePasswordAuthenticationToken
will be set on the SecurityContextHolder
by the authentication Filter
.
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 aUserDetails
that contains user’s correct login information, so that it can be checked against by theDaoAuthenticationProvider
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 |
|
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 |
|
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:
- Inject a bean with
HttpSessionEventPublisher
- This will notify Spring Security session registry when a session is destroyed
- Use the
configure()
method withsessionManagement().maximumSession(<number>)
For example, extending the example from above sections:
1 |
|
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 |
|
Then, if you want to change the session timeout period, you can do so:
1 | server: |
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:
- Obtain the
servletContext
and callsetSessionTrakcingModes()
- 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 | true = |
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 optionsessionFixation()
with the desired strategy
For example:
1 | http.sessionManagement() |
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:
- 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
- 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
- This is harder to setup, yet it is a more secure way for using
Remember Me - Cookie
In summary, all you need to do is:
- Use the
configure()
method to configurerememberMe()
- Add a checkbox with
name=remember-me
specified- The parameter name
remember-me
can also be configured in the first step
- The parameter name
For example, your can have:
1 |
|
Then your html code could look like:
1 | <form name='f' action="/login" method='POST'> |
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 | <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:
- Create
AuthenticationSuccessListener
andAuthenticationFailureListener
s to listen for the authentication event- The two listeners of course implements their corresponding event listener interfaces. Check the examples below.
- Create a
LoginAttemptService
, which connects to yourredis
to read/update/delete login attemps information based on IP address - Update the
isAccountNonBlocked()
method in yourUserDetailsImpl
class based on the login attemps
For example, your two listeners look like:
1 |
|
and
1 |
|
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 | /** |
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 | public class UserDetailsImpl implements UserDetails { |
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:
- Add an additional
roles
column in your database for storing user informations- You can also use a separate table, if you want
- Create/update your corresponding database mappers,
UserDetailsImpl
, andUserDetailsService
- Specifically, implement the
getAuthorities()
method in theUserDetailsImpl
- Specifically, implement the
- Update your
config()
method withantMatcher().hasAuthority()
to specify which resource is available to which authority- More advance matching include using
access(<SpEL>)
, and of coursehasAuthority
can be included in that<SpEL>
- For more details on those
SpEL
, please visit https://docs.spring.io/spring-security/site/docs/current/reference/html5/#el-access
- More advance matching include using
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 correspondingROLE_ADMIN
authority.
For example, your updated database should look like:
1 | id name password roles |
and your mechanism for fetching the roles from the database could be:
1 | public List<SimpleGrantedAuthority> getAuthorities(String username){ |
Now, importantly, you update your UserDetailsImpl
class, as this is the class that Spring Security uses to get data about a user:
1 | package com.example.testspringsecurity.dto; |
Lastly, you customize which resource is limited to which role within the configure()
method:
1 | protected void configure(HttpSecurity http) throws Exception { |
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 complexGrantedAuthority
as aString
would be quite difficult, and as a result thegetAuthority()
method should returnnull
. This will indicate to anyAccessDecisionManager
that it will need to specifically support theGrantedAuthority
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 anAccessDecisionManager
decide, by reading from theGrantedAuthority
, 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 | void decide(Authentication authentication, Object secureObject, |
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 theMethodInvocation
for anyCustomer
argument, and then implement some sort of security logic in theAccessDecisionManager
to ensure the principal is permitted to operate on that customer. Implementations are expected to throw anAccessDeniedException
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.
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 | int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs); |
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 moreACCESS_GRANTED
votes were received (i.e. a deny vote will be ignored, provided there was at least one grant vote). Like theConsensusBased
implementation, there is a parameter that controls the behavior if all voters abstain. - The
UnanimousBased
provider expects unanimousACCESS_GRANTED
votes in order to grant access, ignoring abstains. It will deny access if there is anyACCESS_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 ofVoters
, and decide whether if to throwAccessDeniedException
.
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 | public class RoleVoter implements AccessDecisionVoter<Object> { |
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.
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
” totrue
(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
- First, the
FilterSecurityInterceptor
obtains an Authentication from theSecurityContextHolder
. - Second,
FilterSecurityInterceptor
creates aFilterInvocation
from theHttpServletRequest
,HttpServletResponse
, andFilterChain
that are passed into theFilterSecurityInterceptor
. - Next, it passes the
FilterInvocation
toSecurityMetadataSource
to get theConfigAttribute
s. - Finally, it passes the
Authentication
,FilterInvocation
, andConfigAttribute
s to theAccessDecisionManager
.- If authorization is denied, an
AccessDeniedException
is thrown. In this case theExceptionTranslationFilter
handles theAccessDeniedException
. - If access is granted,
FilterSecurityInterceptor
continues with the FilterChain which allows the application to process normally.
- If authorization is denied, an
For example:
1 | protected void configure(HttpSecurity http) throws Exception { |
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') |
An example usage would be:
1 | protected void configure(HttpSecurity http) throws Exception { |
where:
- any request to
/api/*
will need anadmin
that isfully 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:
- 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
- If that class is not injected as a bean, the
- Refer to that method with
@<beanName>.<methodName()>
For example, you can have:
1 | protected void configure(HttpSecurity http) throws Exception { |
Then this is the checkResouce2()
method inside the MyWebSecurity
class
Note:
- If the class has name
MyWebSecurity
, then the bean has namemyWebSecurity
, with the first letter being lower case.
1 |
|
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 | http |
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:
Add the annotation:
1
2
3
4
5
6
7
8
9
// enable the annotations listed above
(
prePostEnabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
}Make sure the condition does not collide with your
config()
method inside the class above (in which the access logics specified in theconfig()
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 | "hasRole('USER')") ( |
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 | "hasPermission(#contact, 'admin')") ( |
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 ThePermissionEvaluator
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 argumentFor example:
1
2"#c.name == authentication.name") (
public void doSomething(@P("c") Contact contact);
You can also use the Spring Data
@Param
annotation on an argumentFor example:
1
2"#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 aAccessDeniedException
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 | "hasAuthority('USER')") ( |
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"filterObject != authentication.principal.username", (value =
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 | boolean hasPermission(Authentication authentication, Object targetDomainObject, |
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 |
|
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 |
|
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 | public interface BankService { |
and the equivalent @PreAuthorize()
version would be
1 | public interface BankService { |
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:
- Create a
UserDto
(User Data Transfer Object), to encapsulate the information we need from the user - 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
- Note that for the data binding to occur successfully, you need to specify the
- Create your controller class, to return the registration page for
GET
request, and to get the data for thePOST
request - 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.
- 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 | public class UserDto { |
where we see that four input fields - username
,email
, password
, matchingPassword
- will be needed.
Therefore, our corresponding html
page would look like:
1 | <html xmlns:th="http://www.w3.org/1999/xhtml"> |
Notice that:
- Even if there is no
formRegister()
in theconfigure()
method for Spring Security filters, allPOST
request, regardless where it comes from, will need to have a_csrf
token (unless you docsrf().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 |
|
Notice:
- We did not use
@RequestBody
, but@ModelAttribute
annotation for data binding theUserDto
. This is because (in my setup), using@RequestBody
results in an error ofMediaType
not supported. This is becausePOST
request sent data in the form ofFORM_URLENCODED_VALUE
, which is accepted if you use@ModelAttribute
for binding, but not recognized if you use@RequestBody
, which takesapplication/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 | <dependency> |
Yes, dependencies from hibernate
are necessary as well.
Now, you can create your own annotation interface:
1 | // specified where this annotation can be applied (ElementType.FIELD) |
Since we specified @Constraint(validatedBy = CustEmailValidator.class)
, we need to of course implement the validation logic with another class CustEmailValidator
:
1 | public class CustEmailValidator implements ConstraintValidator<CustEmailValidation, String> { |
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
, theAnnotationName
isCustEmailValidator
- Since the target you will wrap around will be
String email
, which is of typeString
, yourEnactedFieldType
will beString
- Since this annotation is called
The
isValid()
method will have the first argument a reference to your target object, in this case,String s
will hold the data of theString 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 | public class UserDto { |
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 | "/register") ( |
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:
- Create a custom
ApplicationEvent
(I called itOnRegistrationCompleteEvent
) 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.
- Another way to do this would be directly program the implementation of setting up the
- Create a custom
ApplicationListener
that listens to the above event you published. In that listener, you configure yourUUID
andusername
pair generation/storage and email sending mechanism- Of course, to setup the above, we need some additional simple objects
- Detailed email setup can be found at https://www.baeldung.com/spring-email
- Create a class that stores the UUID and
username
to activate pair, such asMyVerificationToken
- Update your
controller
that maps toPOST
request for registration, so that it includes the ability to initialize the token and publish the event you created in step 1 - Update your database table to have the field
enabled
, to indicate whether if the account has been activated. You should also update yourUserDetailsService
andUserDetailsImpl
to reflect theenabled
field status - Create corresponding
service
classes, to set/update theenabled
field for a user - Create corresponding
service
classes, to store and look upUUID
andusername
pair in thatMyVerficationToken
- Here I used
redis
for storing the pair. It is convenient to me because I can also setup an expiration time easily
- Here I used
- Create your
GET
controller for thatconfirmRegistration?token=
link, which uses the above services above to check and update theenabled
status of a user- Don’t forget to configure your Spring Security to allow unauthenticated users to access this URL!
- 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 | /** |
Now, you setup the corresponding listener:
1 |
|
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 | public class MyVerificationToken { |
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 |
|
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 | public boolean isEnabled(String username){ |
and your UserDetailsImpl
which is used by Spring Security to check account authentication will look like:
1 |
|
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 |
|
Lastly, your controller
for confirming the link that is sent could look like:
1 | "/confirmRegistration") ( |
Optionally, the listener for that built-in AuthenticationFailureDisabledEvent
would look like:
1 | /** |
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:
- Create the class
CustomAuthenticationProvider implements AuthenticationProvider
, and override thepublic Authentication authenticate(Authentication authentication) throws AuthenticationException
method- 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.
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
public class CustomAuthenticationProvider implements AuthenticationProvider { // extends AbstractUserDetailsAuthenticationProvider
private HttpServletRequest request;
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)
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
- basically, the
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
23public class WebFiltersConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
//..
}
/**
* Use my authentication provider, so that I could intercept for thirdparty user logins
*/
public CustomAuthenticationProvider authProvider() {
return new CustomAuthenticationProvider();
}
// add the providers into the manager
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authProvider());
}
// other customized settings
}
Mechanism Behind the Above Implementation
In short, this is what happens:
- Spring Security gets the authentication request, and go to the class
ProviderManager
to callauthenticate
. - The above step will get a list of authentication providers, and iterate through them with
Iterator var8 = this.getProviders().iterator();
, callingauthenticate
method for eachAuthenticationProvider
.- e.g. the
DaoAuthenticationProvider
and theCustomAuthenticationProvider
you implemented above
- e.g. the
- If the certain provider returned
null
, then Spring checks if there is aparent
of thatAuthenticationProvider
.- in this case, the
CustomAuthenticationProvider
is standalone. Hence whatever you returned there will be the final returned result.
- in this case, the