Spring Security + JWTs getting started
JWTs are becoming increasingly popular on the internet especially with the javascript community. This post will focus on how to enable the support for handling JWTs in spring security. But before we start we need to talk a little about what a JWT is, how it works, what are some of good usages and what are some bad usages of JWTs.
What is a JWT?
A JWT stands for JSON Web Token and is a cryptographically signed json object. Also known as a “token”. Its full specification is defined in a document called RFC7519 https://www.rfc-editor.org/rfc/rfc7519. There are several
types of JWTs, but we are going to focus on the most used ones called signed tokens (JWS). There are also tokens when the entire token is encrypted and these are called JWEs, but we won’t be cover them today.
Both these token types are essentially JSON objects containing a number of parameters called claims because when presenting a token, the information inside it is claiming to be someone.
So what does a JWT look like? well here is an example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
What you see above is a base64 encoded JWT token.
The token consists of 3 parts, and the parts are separated by a tiny little dot character. First part is called the JOSE header which contains meta data, the second part is the body containing the general information (claims), and last part is the signage.
JOSE Header
When you base64 decode the first part of the JWT you will get something that looks like the below example:
{
"alg":"HS256",
"typ":"JWT"
}
This is the first part of the JWT and when decoded it contains a JSON object that has metadata about the token. For instance, it contains the alg property telling us what algorithm was used when this token was signed. And then we have the typ property that tells us this is a JWT and not a JWE.
The alg property must always be present, and it is up to the application that is receiving the token to decide what algorithms are accepted.
Note that you can send non-signed JWTs by setting the alg parameter to none. This is very insecure, because this basically means that anyone can change the information in the JWT and we would still consider it as valid. The whole point of signing a JWT is so that the information in the token can’t be manipulated.
All JWTs using none as alg should always be rejected at all times.
Claims
A JWT contains a set of defined claims. Some claims are registered claims which means that they have special meaning if used, but actually no claims are mandatory according to the rfc.
You can basically define whatever you want in a JWT there are no rules. Also, just to mention, a claim is essentially just a fancier name for a property in the body.
But as mentioned earlier there are some registered claims and i’m going to go through some of the more common ones that you usually get to see so you’ll get a basic understanding for them.
Below is a an example body:
{
"sub":"1234567890",
"name":"John Doe",
"iat":1516239022
}
sub (subject)
contains usually some type of value that you can use to identify the user of this claim. Typically, this is some UUID or some random number. What is important is that this value needs to be locally unique in the context of the issuer, or globally unique. Which means if you have someone creating a JWT in a private network the subject needs to be unique within that private network.
Or, if this JWT is sent out on the internet, it needs to be globally unique so it doesn’t collide with something else out there. Use of this claim is optional.
iss (issuer)
This value identifies who issued the JWT. Here you can either find a case-sensitive string or a URI that points us to what service that issued this particular JWT. This claim should be verified to make sure that we only accept JWTs from a specific issuer. Use of this claim is Optional.
iat (issued at)
The iat claim contains a value that identifies at what time this JWT was issued. This claim can be used to determine the age of the JWT. This value needs to be in NumericDate format according to the rfc which means it has to be a numeric value representing the number of seconds since 1970–01–01T00:00:00Z UTC until the specified UTC date/time, ignoring
leap seconds. The use of this claim is optional.
exp (expiration time)
The exp (expiration time) claim tell us at which time this JWT expires and servers must not read the JWT after this set time. Processing of this JWT is only allowed if the current date is before the date time given in the exp claim.
Services and code implementers may give some small leeway to account for clock skew, but usually no more than a couple of minutes. Use of this claim is optional and the value format has to be, like in the iat claim, a NumericDate which is the number of seconds since 1970–01–01T00:00:00Z UTC, ignoring leap seconds.
There are several more registered claims but these are the more common ones, and we will probably see some additional ones later when we start writing our implementation.
Before we can start implementing we need to talk a little about a different specification.
Oauth2
Oauth2 is the de facto industry standard for online authorization. It's a specification that defines different ways of authentication and then let users access secured resources. The standard is defined in the rfc6749 https://www.ietf.org/rfc/rfc6749.txt and is quite a large standard.
In this post we are not going to go through the part of the standard that defines the different ways on how to authenticate (because there are many ways) we are instead just going to focus on the part of what happens after you have authenticated and you are issued a token. What happens then?
Worth mentioning is that the rfc standard for Oauth2 talks about "tokens" as in any token. It's important to point out that Oauth2 works without JWTs. And JWTs work without Oauth2. JWTs are just a token format, that we happen to use with Oauth2, but there are several other token formats out there. And there are several other usages of JWTs that has nothing to do with Oauth2.
Resource Server
In the oauth2 standard they define something that is called a resource server. This is usually the server that has some resources that you want access to (for instance a REST api). So this is who you present your JWT to. The resource server then needs to verify the token, and if the token is valid, it will present you with the data you asked for.
Springs documentation uses this terminology in the documentation and their entire chapter on JWTs is defined under the Oauth2 resource server chapter. https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html
I do want to point out that sometimes, when I mention that someones application is a resource server, they will respond “I don’t want Oauth2 I just want to use JWTs”.
But they don’t really understand that presenting a token to a server is part of the Oauth2 standard. They believe that Oauth2 only has to do with login flows, but it also handles flows on how authorize users when retrieving data an lots of other things. It’s a huge specification!
I usually have to explain to people that they are implementing something a small part of the Oauth2 spec (page 48, section 7 of the rfc) and not necessarily implementing the entire oauth2 specification.
A small history of JWTs in spring security
Spring support for oauth2 was released on the 7 november 2012 and was released in a separate packaged named `spring-security-oauth2` https://github.com/spring-attic/spring-security-oauth . This package contained functionality to create an authorization server that could issue tokens, but also the ability to enable your applications to become resource servers and accept tokens.
The JWT specification was released on the 28 of December 2010 (almost 2 years before the release of spring-security-oauth2) and JWT support was lacking in spring security. The first release of JWT support was released in March 2013 but it was common knowledge that it was tricky to get oauth2 support working with Spring and then also adding JWT on top of it made things even more difficult.
Most of the entire Spring Oauth2 and JWT code base was maintained by a single person which resulted in not many examples to look at, and the documentation was quite sparse.
But you have to remember that back in those days Oauth2 wasn't that wide spread yet, and JWTs weren't either, but as more and more people started using it, many complained that the support was overly complicated.
So what happened was that the community implemented Oauth2 resource servers in spring but then added the JWT support by writing their own custom JWTFilters to handle the new “cool” token format.
When Spring Security 5 was released (November 28, 2017) Spring announced that the old spring-security-oauth2 library got placed in maintenance mode, which basically meant that they would only update if any severe security patches were needed but no new features would be added.
Spring Security 5 would have full oauth2 resource server support while
spring would drop all support for authorization server (spring did change their mind at a later date and now maintains a fully open sourced authorization server, check it out).
So now there is full support for Oauth2 resource servers and full support with a built in JWTFilter per default in Spring Security, which means there is no need to implement your own custom one anymore.
But sadly, still to this day, many think that you need to implement your own JWTFilter and the top searches on Google for "spring security jwt" returns many faulty tutorials building custom JWTFilters without knowing that there is full support already:
- https://www.toptal.com/spring/spring-security-tutorial
- https://www.javainuse.com/spring/boot-jwt
- https://www.codejava.net/frameworks/spring-boot/spring-security-jwt-authentication-tutorial
- https://medium.com/geekculture/implementing-json-web-token-jwt-authentication-using-spring-security-detailed-walkthrough-1ac480a8d970
Sadly all of the above above links lead to tutorials that implement custom handling of JWTs. Writing custom security is in general bad practice and Spring Security's JWT implementation is used by tens of thousands of applications in production environments world wide and is fully open sourced so i would recommend using that than any custom implementation off the internet.
Spring Security JWTs getting started
So lets start building our service. First go to spring initlzr and create a new project. We need to add some dependencies to our project.
we need the following ones:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server
</artifactId>
</dependency>
I’m using Maven here but you can use Gradle or anything else. Most of these are self explanatory, there is a simple starter to enable oauth2 resource server support in our application. It will pull in another dependency called nimbus-jose.
nimbus-Jose is a JWT library that comes packaged with spring security https://bitbucket.org/connect2id/nimbus-jose-jwt
Enable the JWT support
The first thing we want to do, is to enable the Spring Security JWT support. We do that by basically telling the framework that this is a resource server and that we are going to accept JWTs.
So lets create a SecurityFilterChain
in a configuration class and configure it using the oaut2ResourceServer
and then enable jwt
handling.
@Configuration
public class SecurityConfig { @Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests(authorize -> // Here we set authentication for all endpoints
authorize.anyRequest().authenticated() )
// Here we enable that we will accept JWTs
.oauth2ResourceServer(configure -> configure.jwt(Customizer.withDefaults()))
.build();
}
If we now do a simple http request to our application using curl we get the following result:
$ curl -I localhost:8080
HTTP/1.1 401
WWW-Authenticate: Bearer
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Transfer-Encoding: chunked
Date: Thu, 01 Sep 2022 22:36:27 GMT
This service is responding with a 401 Unauthorized and the header WWW-Authenticate: Bearer
which means that it accepts tokens, but we requested something without supplying one.
Now that the server is willing to accept JWTs we can add some sort of endpoint that we can call.
Adding a simple endpoint
Lets add an endpoint that we can call that will give us back the parsed JWT, so we can actually see what Spring Security has read from it.
@RestController
public class MainController {
@GetMapping("/token")
public Token getToken(JwtAuthenticationToken jwtToken) {
return new Token(
jwtToken.getToken(),
jwtToken.getAuthorities()
);
} public record Token(Jwt token, Collection<GrantedAuthority> authorities){}}
This adds a simple endpoint that will inject the JwtAuthenticationToken
that Spring Security has parsed, and from that we can extract some of the information. We place that information into a java record that we can return to the calling client. Why i chose to to this repackaging is mainly because the raw JwtAuthenticationToken
class contains a lot of unnecessary meta data so we can reduce it down a bit.
If we now call our endpoint, we will not be able to reach it since it is protected behind a JWTFilter, o now we need to configure a JwtDecoder
to be able to start decode and validate tokens.
Adding a JWT Decoder with standard validation
Every JWT is signed, so we need a key to validate the signature. You can use eithera JWK (Java Web Keys) a SecretKey
(shared secret, which means you and whom created the token, have the same key, also known a a symmetric key) or as we are going to do, add a RSAPublicKey
that we can use for the verification which is a public/private key (asymmetric key).
You need to generate this key using OpenSSL and there are several tutorials out there explaining how to do it, but in the gitrepo that has the finished application https://github.com/Tandolf/spring-security-jwt-demo/blob/main/src/main/resources/jwtRS256.rsa.pem there is already a key we can use. Also in there are the private/public keys that i used to generate all the demo JWTs that you can find in the readme file in the root of the project.
Lets update and add the ability to read the key from our application.yml
file.
@Configuration
public class SecurityConfig { // the value we need to set in our application.yml
@Value("${key.location}")
private RSAPublicKey key; @Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// our earlier configuration }
}
We also need to point out in our application.yml
that we want to load our key.
application.ymlkey:
location: jwtRS256.pkcs8.pem
Now that we have loaded our public key that we are going to use to verify the signatures we can now configure a JWTDecoder
that will use this key to do the verification.
Just add a new bean to our configuration class.
SecurityConfig.class@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}
Above we create a NimbusJwtDecoder
and set it to use our public key, and we then build it and give it to Spring which will add this automatically into the mix.
Time to do our first valid request!
First valid request
To do requests we can use any http client, there are several out there and im going to use curl, but you can use anything, like postman, insomnia etc:
curl --oauth2-bearer "<token>" http://localhost:8080/token | jq .
Just replace the <token>
with one of the tokens that is supplied in the git repository readme file https://github.com/Tandolf/spring-security-jwt-demo#scope-based-jwts
By piping the returned json value to jq
we can get a pretty formatted json string out of it.
Response should look something like the following example:
{
“token”: {
“tokenValue”: “<token>”,
“issuedAt”: “2018–01–18T01:30:22Z”,
“expiresAt”: “2049–03–22T04:26:40Z”,
“headers”: {
“typ”: “JWT”,
“alg”: “RS256”
},
“claims”: {
“sub”: “foo”,
“aud”: [
“foobar”
],
“scope”: “read”,
“iss”: “http://foobar.com”,
“name”: “Mr Foo Bar”,
“exp”: “2049–03-22T04:26:40Z",
“iat”: “2018–01–18T01:30:22Z”
},
“subject”: “foo”,
“notBefore”: null,
“issuer”: “http://foobar.com",
“audience”: [
“foobar”
],
“id”: null
},
“authorities”: []
}
First and foremost the jwt validation was successful, otherwise our JWT wouldn’t have been accepted in the first place. And second we can also see that Spring Security parsed our token, which contained a number of different claims.
You can see that spring stores the raw token, and also the raw claim values. But then it also shows us what it has parsed the claims into. So for instance iat
has been parsed into issuedAt
You can also see that sub
has been parsed into subject
etc. etc.
One thing to point out is that Spring Security did not find any authorities
per default. Scopes don’t get parsed into authorities per default. But we will talk more about that in the Configure Authorization paragraph below.
Custom validation
So now we can send and validate the integrity of our token, but what if we have a custom claim value in our JWT that we want to validate?
Per default, Spring Security always validates the iat value so that if the token has expired it gets rejected. But we can replace the validation and have our complete own validation instead. Spring Security allows us to fully customize our validation using a mixture of the default validations, and our own custom validations.
Lets add some custom validation!
For this task we can use the Oauth2TokenValidator<T>
interface. There are a couple of classes that already implement this interface and if we want to validate a specific claim, we can use the JwtClaimValidator<T>
class. This interface accepts a claim type and a Predicate<T>
that needs to evaluate to true or false to pass validation.
Here is an example:
SecurityConfig.classpublic OAuth2TokenValidator<Jwt> audienceValidator() {
return new JwtClaimValidator<List<String>>(
OAuth2TokenIntrospectionClaimNames.AUD,
aud -> aud.contains(“foobar”)
);
}
So here i create a JwtClaimValidator<List<String>>
that is generic over a List<String
. The first inparameter is what claim we want to validate. There are a lot of predefined claims in the Oauth2TokenIntrospectionClaimNames
class, so i pass in AUD
(which is audience) and then as the second parameter i create my predicate. Above we just check that the list of all aud claims contains the name “Foobar” (the audience claim can contain many values).
If it evaluates to true we pass the validation, if not, we fail and return.
Now that we have our validator, we need to hook it into the JwtDecoder that we created earlier.
But before we do, we need to group our validators together. We do that by creating a DelegatingOauth2TokenValidator
All of this sounds a lot more complicated than it is but it’s basically just a list of validators that we are going to place in our JwtDecoder.
SecurityConfig.classpublic OAuth2TokenValidator<Jwt> tokenValidator() {
final List<OAuth2TokenValidator<Jwt>> validators =
List.of(new JwtTimestampValidator(),
new JwtIssuerValidator("http://foobar.com"),
audienceValidator());
return new DelegatingOAuth2TokenValidator<>(validators);
}
Here we create a list and include our validator, i have also made sure that we still have our TimestampValidator
and i also added just for fun the built in JwtIssuerValidator
.
It’s strongly recommended to not have tokens that can live forever. It’s a huge security risk. Also its strongly recommended to validate the issuer so that you only accept tokens from a known source of creation.
We are almost there now, all we need to to is to hook in our DelegatingOAuth2TokenValidator
to our JwtDecoder
SecurityConfig.class@Bean
public JwtDecoder jwtDecoder() {
final NimbusJwtDecoder decoder = NimbusJwtDecoder
.withPublicKey(this.key)
.build();
decoder.setJwtValidator(tokenValidator());
return decoder;
}
And to do that we just use the setValidator
function on our JwtValidator
we created earlier.
I leave it as an exercise to you to play around and see what happens if you write a validator that will deny a token because of some value.
But that’s it, we have now added custom validation!
Configure Authorization (scopes)
Usually we don’t want every token to have access to every endpoint at all times. So JWTs usually have a scope claim which is a list of names that we associate to different authorities (access rights).
Typical names are read, write, delete
etc etc.
So it could look like this in a jwt:
{
// other claims here above
“scope: “read write”
}
If the scope claim contains values, Spring Security will automatically map these claims to authorities in spring. So what is an authority? well it’s sort of what you are allowed and not allowed to do. Spring also has this concept of what is called Roles
and we are going to talk about the in the next section.
But in general, scopes are fine grained access, while roles are more of a collection of scopes. Think of it this way, you might be able to read but not write to something. But then the administrator is allowed to read and write. So instead of giving you fine grained access, we create 2 roles, User and Admin, where one has read, the other has read and write.
In this tiny example it might sound a bit over engineered, but imagine when you have 20-30 scopes, then it can be quite nice, to group scopes into roles, and then assign roles instead of individual scopes.
So lets create an endpoint, that requires you to have a specific scope set in the JWTs. We update our security config:
SecurityConfig.class@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests(authorize ->
authorize
.requstMatchers("/read/**").hasAuthority("SCOPE_read")
.requestMatchers("/write/**").hasAuthority("SCOPE_write")
.anyRequest().authenticated()
)
.oauth2ResourceServer(configure -> configure.jwt(Customizer.withDefaults()))
.build();
}
Here i add security constraints to 2 new endpoints. One has to have the scope read
and the other needs to have the scope write
. Take notice that i needed to add the prefix SCOPE_
, This is so that we can distinguish between scopes and roles later (roles are prefixed with ROLE_).
We also need to actually create our new endpoints:
MainController.class@GetMapping("/read")
public String read() {
return "Welcome to the internet, i'll be your guide";
}
@GetMapping("/write")
public String write() {
return "I know kung fu!";
}
Now if we try to make a request with a scopeless JWT (check the readme in the github project for the jwts) we get the following:
$ curl -I — oauth2-bearer <token> localhost:8080/read
HTTP/1.1 403
WWW-Authenticate: Bearer error=”insufficient_scope”, error_description=”The request requires higher privileges than provided by the access token.”, error_uri=”https://tools.ietf.org/html/rfc6750#section-3.1"
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Transfer-Encoding: chunked
Date: Sun, 04 Sep 2022 13:02:49 GMT
You can see we get an error back with some information in the WWW-Authenticate: Bearer
header which tells us that we don’t have the scope
required to perform the given request. It even points us to the rfc specification where we can read more about it.
Lets do a request that has the sufficient scope:
$ curl — oauth2-bearer <token> localhost:8080/read
Welcome to the internet, i’ll be your guide
Which works fine. So all you need to do is to set an hasAuthority
in the security configuration and make sure that the scope defined is prefixed with SCOPE_
. Then make sure that the JWTs contains the claim scope
and that those scopes match.
In the github repo you can find both tokens contain read and write scopes, and the security configuration.
How to setup Spring Roles from a JWT
Another very common user case is that you get a set of roles in a JWT, or you just want to map scopes into spring roles. We are going to address both of these user cases now.
Spring Security has a class called JwtAuthenticationConverter
and its purpose is to take whatever claim in the JWT and map/convert these into your authority of choice. It’s very flexible and useful.
Fir instance, lets say that the JWT does not contain a scope
claim but an authorities
claim. How to we tell Spring Security, that we don’t want the defaults, but we want to point out our own custom claim.
We can do that quite easy by creating a converter and give that to the framework.
SecurityConfig.class@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
final JwtGrantedAuthoritiesConverter gac = new JwtGrantedAuthoritiesConverter();
gac.setAuthoritiesClaimName("authorities");
gac.setAuthorityPrefix("ROLE_");
final JwtAuthenticationConverter jac = new JwtAuthenticationConverter();
jac.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jac;
}
By creating a converter we can for instance tell it what the claim name is, here i tell it authorities
is the proper claim name. I also add a prefix to define that the values loaded are roles
and not scopes
i then place this converter into a JwtAuthenticationConverter
and return this into the framework as a Bean
.
And just like that, the framework will read a different claim, prefix the values in that claim, and create spring GrantedAuthorities
(roles) for me to use.
We can try this out, by adding endpoints that needs roles. And try it out with some of the JWTs provided in the github repo.
SecurityConfig.class@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests(authorize ->
authorize
.requestMatchers("/user/**").hasAnyRole("user", "admin")
.requestMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated()
)
.oauth2ResourceServer(configure -> configure.jwt(Customizer.withDefaults()))
.build();
}
And just like before we can add the protected endpoints:
MainController.class@GetMapping("/user")
public String user() {
return "You can't judge me, i am justice itself";
}
@GetMapping("/admin")
public String admin() {
return "All your base are belong to us";
}
In the converter you can write any logic you want if you want to group scopes together into roles, or if you wish to map scopes into roles etc. So depending on where you get the authorities from, Spring Security is very flexible in how you can convert them from one format to another.
Enable debug logs
If you during any time have any problems its always good to know how to enable debug logs for Spring Security. If you get for instance a 403 or a 401 always check the logs because there is a highly likelihood that there will be an exception there with a clear error message of what went wrong.
To enable the logs you can add the following in the application.yml
application.ymllogging:
level:
org:
springframework:
security: TRACE
I have set the logs to TRACE
but other valid values are ERROR, WARN, INFO, DEBUG and TRACE
.
An example of an error when the validation fails:
org.springframework.security.oauth2.server.resource.InvalidBearerTokenException: An error occurred while attempting to decode the Jwt: The aud claim is not valid
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]...
Caused by: org.springframework.security.oauth2.jwt.JwtValidationException: An error occurred while attempting to decode the Jwt: The aud claim is not valid at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.validateJwt(NimbusJwtDecoder.java:189) ~[spring-security-oauth2-jose-5.7.2.jar:5.7.2] at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.decode(NimbusJwtDecoder.java:138) ~[spring-security-oauth2-jose-5.7.2.jar:5.7.2] at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.getJwt(JwtAuthenticationProvider.java:97) ~[spring-security-oauth2-resource-server-5.7.2.jar:5.7.2]
… 58 common frames omitted
Spring gives very explanatory errors which will help you a lot if you enable debugging logs.
Summary
If you have been with me all the way to the end, i salute you.
I wrote this because there are a lot of resources out there that propose that you write custom security which is a huge problem today in society.
Custom security usually end up easier to breach and the risk of data loss and personal information loss is potentially higher.
Open sourced security solutions like Spring Security is scrutinized by thousands of people and that’s why we as developers need to stick to building security that follows the standards and guidelines set out there.
One more tutorial/blog might not change the world, but i hope that you that read this gets a better understanding of how Spring Security works, and that it is not as complicated as you think it is,