Spring Security 5: Authentication with Basic Auth and JWT

von

All articles in this series

Spring Security 5

If you are here for the first time, you should check out our earlier articles on Introduction to Spring Security 5 and authenticate users with JDBC.

The previous articles explained the basics of Spring Security and we looked at connecting to JDBC databases. If you’re not very familiar with all of that and WebSecurityConfigurerAdapter, you should probably read those articles first.

Basic Auth? JWT? SPA?

Today applications often use a Single Page Application (SPA) for their frontend. There used to be a time when clicking a link meant the whole website disappeared for a short while and reappeared, freshly created by the webserver. This approach is no longer preferred. Once the page is initally loaded, only the content that needs to be updated would be replaced. This is all made possible through Javascript, the result is a fast, and reactive website.

With this new approach backend servers have changed to pure content delivery instead of creating HTML. This has also lead to a change in how web applications perform authentification. Instead of sending HTML forms to the webserver it is more efficient and secure to just send the authentication information in the header of an HTML request and the server does the rest.

In many cases, the webserver follows the REST approach of defining what needs to be sent, and what to accept. This type of method of a webserver is known as an end point and is known as an Application Programming Interface (API).

Basic Auth

Each time a request is sent to the server, it would need to be authenticated so that the application can ensure that the request is from a valid user and identify the user. The easiest way to do this is by sending the username and password with each and every request. Theoretically, one could create some kind of session and store this information in a cookie. But sessions are hard to maintain when the application grows with a large number of users and also in cases where there is one than more backend server. So there needs to be a better way.

At first thought, you may think of sending the credentials to the server as a JSON string with the request params. This would mean that authentication has to be handled in every method.

The solution is to use Basic Auth, which requires sending the credentials with every request, but as a header.

An example:

Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l

If you recognized the last part of the string as an encoded string, you are on the right track. We encoded Aladdin:OpenSesame (username:password) and it is Base64 encoded. If there is no encryption, the password will be transferred as clear text.

Compared to our approach in the earlier articles, there are only a few changes to the code.

The following code enables Basic Authentication using httpBasic() instead of formLogin(). By specification you have to give a name to a “realm”, which is basically the name of the space you want to protect.

These realms allow the protected resources on a server to be partitioned into a set of protection spaces, each with its own authentication scheme and/or authorization database. – RFC 2617

The implementation looks like this:

@EnableWebSecurity
public class BasicSecurityConfiguration extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
    .authorizeRequests(authorizeRequests ->
        authorizeRequests
                .antMatchers("/board/*").hasAnyRole("MEMBER", "BOARD")
                .antMatchers("/members/*").hasRole("MEMBER")
                .antMatchers("/").permitAll()
    )
    .httpBasic().realmName("My org ream")
  }
  ...
}

This code is very similar to the one we used in the first article. So if you are not sure what this code means, it’s best that you read that article.

One more thing. To prevent session cookies from being stored in the browser, it is beneficial to disable sessions for your requests. In general, sessions are complicated and can open security risks in your application, but that’s a story for another post. Here is the full class, including the disabled sessions.

@EnableWebSecurity
public class BasicSecurityConfiguration extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests(authorizeRequests ->
            authorizeRequests
                    .antMatchers("/board/*").hasAnyRole("MEMBER", "BOARD")
                    .antMatchers("/members/*").hasRole("MEMBER")
                    .antMatchers("/").permitAll()
    )
        .httpBasic().realmName("My org ream")
        .and()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    ...
}

Why JWT?

Now, the basic auth approach is fine for a small application with only a few end points, especially if your backend server are SSL certified. But generally, sending the password in each of your requests is not recommended and will make many users uncomfortable. Also, browsers might do things you don’t expect (like caching). Generally speaking, if you want to open your API to the public you might need to build your application better.

So, what if you send the username and password only once and get a token with a limited lifespan. You could then just send the token in an encrypted form and the server can make sure that this token is valid. Even if someone picks up the token, there isn’t much they can do as it will expire soon.

That’s exactly what JWT does. What’s more, you can even pass some meta information about the user as well.

And here comes the best part, since a JWT token is just some encrypted text, there is absolutely no need for complex OAUTH or other third party servers. We can do everything in our own code and we don’t even need to store the token.

JWT dependencies

We’ll be using the library jsonwebtoken.io for this article. It’s not impossible to do this with just Spring Security. But JWT is very simple (compared to other solutions), so we’ll be using it.

Let’s start by adding the required dependencies:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.10.7</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.10.7</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.10.7</version>
    <scope>runtime</scope>
</dependency>

I added some dependencies with the “runtime” scope. You can read about the Maven scopes online.

In short, runtime means they are included in the final result, but you cannot really use code directly from it.

JWT Filters

One important thing to know about Spring Security is that it uses Servlet filters. You can imagine a filter, like the one you find in a coffee machine. You pour water over it, and the filter modifies the water (mixes it with coffee) and lets out some kind of modified fluid: coffee.

The same happens with Spring filters. A web request comes in and is passed into a filter. It will be inspected, things like authentication will be done and eventually the request is passed - or is denied. A filter is a very generic approach to a specific problem. So, it is very flexible.

To make JWT happen, we need to implement two filters on our own as there are no available implementations (to my knowledge).

The first one is an authentication filter, and the second one is an authorization filter.

JWT Authentication Filter

Looking through Spring Security you will find a class called UsernamePasswordAuthenticationFilter. This class does everything we need so we can extend from it.

The minimal version should look like this:

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.setAuthenticationManager(authenticationManager);
    }
}

This code basically sets the authentication manager which was configured to override configure(AuthenticationManagerBuilder auth). If you aren’t exactly sure which method, it is the one with the JDBC code to connect to a database for user authentication.

By default, UsernamePasswordAuthenticationFilter has the authenticationManager field as private, so we need to add a setter.

With calling:

curl -i -X POST http://localhost:8080/login\?username\=emma\&password\=emma

This takes us to the parent class with a method called attemptAuthentication. It looks like:

public Authentication attemptAuthentication(HttpServletRequest request,
        HttpServletResponse response) throws AuthenticationException {
    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException(
                "Authentication method not supported: " + request.getMethod());
    }

    String username = obtainUsername(request);
    String password = obtainPassword(request);

    if (username == null) {
        username = "";
    }

    if (password == null) {
        password = "";
    }

    username = username.trim();

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
            username, password);

    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);

    return this.getAuthenticationManager().authenticate(authRequest);
}

As you can see, this method does all kinds of tasks for us. If we are to alter this behavior, we need to override the implementation. As an example, we could change the method signature from POST to GET. But why would you do that? POST is for actions like saving data and creating objects, and we are going to create a token. So this is the best HTTP method for the job.

Another thing would be to change the authentication URL we want to use. To do so, we can change our code to look like this:

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.setAuthenticationManager(authenticationManager);
        setFilterProcessesUrl("/api/login");
    }
}

The setFilterProcessesUrl method is used to set a URL of our preference for the authentication end point.

That’s it for authentication. But we still need to create a token and return it. We need to override some behavior in our class again.

First, we need to generate a signing key. On StackOverflow I found a simple way to create a key
with HS512.

openssl rand -base64 172 | tr -d '\n'
yiw1z2XJKQ7VHI/ck49j/RUAWm1gmhJ6x0MavXEV2bvHIDNfxXI2s3nCXfD58YYXZW9KYo/OkJmSunGhpJTA4nK53FxVcACt+kf6NhG6VA40gaUGOSnGupPtv8hhLGnKRD9BIjvbhFrMjIkyL4/WGyFObglcnmrxT12z5Cl4Zr6zKKFUfX6W2XXj7VZxGvrXS4vSNWWkBPP117V4+0yiq7/HgjJNAGAL7NrDVg==

Let’s store this and some other values in application.properties:

jwt.secret=yiw1z2XJKQ7VHI...
jwt.issuer=Grobmeier Solutions GmbH
jwt.type=JWT
jwt.audience=MyApp

Awesome. Guess what? we are going to create the filter object ourselves, and not through Spring, so we have to add those values to the constructor.

This is how it should look once you’re done:


public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private String jwtAudience;
    private String jwtIssuer;
    private String jwtSecret;
    private String jwtType;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager,
        String jwtAudience, String jwtIssuer,
        String jwtSecret, String jwtType) {
        this.jwtAudience = jwtAudience;
        this.jwtIssuer = jwtIssuer;
        this.jwtSecret = jwtSecret;
        this.jwtType = jwtType;
        this.setAuthenticationManager(authenticationManager);
        setFilterProcessesUrl("/api/login");
    }

It’s true, it doesn’t look very nice. Next, let’s add the code for the actual token generation:

@Override
protected void successfulAuthentication(
        HttpServletRequest request, HttpServletResponse response,
        FilterChain filterChain, Authentication authentication) {
    User user = (User)authentication.getPrincipal();
    SecretKey secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes());
    String token = Jwts.builder()
            .signWith(secretKey, SignatureAlgorithm.HS512)
            .setHeaderParam("typ", jwtType)
            .setIssuer(jwtIssuer)
            .setAudience(jwtAudience)
            .setSubject(user.getUsername())
            .setExpiration(new Date(System.currentTimeMillis() + 864000000))
            .compact();

    response.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
}

Are you clear what exactly is going on here?

We start by getting the principal object which contains the authenticated user. Then we get a secret key from our JWT secret key string and generate the JWT token. We add the signing key, some headers, and issuer.

Most important: the expiration date. The recommendation is to make it very short, so the authentication happens quite often. I haven’t followed that advice here for simplicity, but you definitely should to do so.

Finally, the call to compact() returns a string token. We can add it to the response with Spring’s own HttpHeaders constants. Don’t forget to prefix it with “Bearer”.

Now, this needs to be added to the security configuration class. Also, we have to read the configuration values using @Value.

The class changes as follows:

@EnableWebSecurity
public class JwtSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Value("${jwt.secret}")
    private String jwtSecret;
    @Value("${jwt.issuer}")
    private String jwtIssuer;
    @Value("${jwt.type}")
    private String jwtType;
    @Value("${jwt.audience}")
    private String jwtAudience;

    ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtAudience, jwtIssuer, jwtSecret, jwtType))
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
                                .antMatchers("/board/**").hasAnyRole("MEMBER", "BOARD")
                                .antMatchers("/members/**").hasRole("MEMBER")
                                .anyRequest().authenticated()
                )
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    ...

Now let’s try this again:

curl -i -X POST http://localhost:8080/login\?username\=emma\&password\=emma

If the authentication is successful, you should recieve a response like this:

HTTP/1.1 200
Vary: Origin
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6ImEiLCJleHAiOjE1NzI3ODk5MDZ9.L1mvztlVEHh7l5s6AtFHDXHUSIk0-fE4Ded7XZvtgwYdQl3Ej5uLkwUnClk4hcCjh840ajJqrwK1YJFC10RmXy
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
...

The long string after the Bearer keyword is the one you’ll need for all further calls to the API. You can decode this string on a website like jsonwebtoken.io and look at its contents.

Please note: some people add some additional information to it, like role information. This isn’t recommended, especially when you add things like roles, as they cannot be changed until the user creates a new token. This might lead to un-authorized access to some pages even after roles have been removed in the server.

That aside, we can successfully authenticate now. What about making actual calls to the API?

JWT Authorization Filter

The next filter to create is one to authorize all requests. There is a “OncePerRequestFilter” which would match, but I have chosen to override the BasicAuthenticationFilter instead. It’s very basic and quite easy to understand. So here we go.

The following code works almost the same as the previous one. I actually added one more thing, which is the JdbcTemplate that I used to select the roles from the database.

public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    private String jwtSecret;
    private String jwtIssuer;
    private String jwtType;
    private String jwtAudience;

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, JdbcTemplate jdbcTemplate,
                                   String jwtAudience, String jwtIssuer, String jwtSecret, String jwtType) {
        super(authenticationManager);
        this.jdbcTemplate = jdbcTemplate;
        this.jwtAudience = jwtAudience;
        this.jwtIssuer = jwtIssuer;
        this.jwtSecret = jwtSecret;
        this.jwtType = jwtType;
    }

The next piece would be to add some code to create a UsernamePasswordAuthenticationToken from the JWT token.

private UsernamePasswordAuthenticationToken parseToken(HttpServletRequest request) {
    String token = request.getHeader(HttpHeaders.AUTHORIZATION);
    if (token != null && token.startsWith("Bearer ")) {
        String claims = token.replace("Bearer ", "");
        try {
            Jws<Claims> claimsJws = Jwts.parser()
                    .setSigningKey(jwtSecret.getBytes())
                    .parseClaimsJws(claims);

            String username = claimsJws.getBody().getSubject();

            if ("".equals(username) || username == null) {
                return null;
            }

            // TODO roles here!

            return new UsernamePasswordAuthenticationToken(username, null, null);
        } catch (JwtException exception) {
            log.warn("Some exception : {} failed : {}", token, exception.getMessage());
        }
    }

    return null;
}

The idea is to somehow read the username with a Jwts parser. The “claims” are the parts of the payload. It contains a subject, which is the actual username. The UsernamePasswordAuthenticationToken should also have access to some roles.

I select those roles using a statement like this, where I put the TODO in my code above:

List<SimpleGrantedAuthority> authorities = jdbcTemplate.queryForList(
    "SELECT a.authority " +
    "FROM user_authorities a, users u " +
    "WHERE u.username = ? " +
    "AND u.id = a.user_id", String.class, username)
    .stream()
    .map(SimpleGrantedAuthority::new)
    .collect(Collectors.toList());

This query returns a list of authorities, stream the result and map them to SimpleGrantedAuthority. Then we create a List of them and put the authorities to the new token:

return new UsernamePasswordAuthenticationToken(username, null, authorities);

Finally, override the doFilterInternal method as well.

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                FilterChain filterChain) throws IOException, ServletException {
    UsernamePasswordAuthenticationToken authentication = parseToken(request);

    if (authentication != null) {
        SecurityContextHolder.getContext().setAuthentication(authentication);
    } else {
        SecurityContextHolder.clearContext();
    }

    filterChain.doFilter(request, response);
}

This is probably the most basic implementation. The one found in BasicAuthenticationFilter is more interesting. Before you try this code, you should take a look at the BasicAuthenticationFilter aswell.

The above code parses a token. If authentication is valid, the authentication context is set - which means the user is authenticated. Otherwise everything is cleared, just like in the parent class.

Let’s move on and continue with the filters, let Spring Security do its job.

Conclusion

To understand that filters do the authentication job basically means you understand Spring Security as a whole. With a proper understanding of filters, you can implement any authentication or authorization mechanism you want.

Spring Security is quite flexible in general. It may not solve every use case, but there aren’t too many that it can’t handle either.

Despite this blog post being quite lengthy, we hope you found this trio of blog posts interesting. Understanding Spring and Spring Security can be quite challenging for a beginner, but having a post that covers all important areas can be quite helpful.

If you can think of any improvement to these posts, that will help another reader, please let me know. I would greatly appreciate it!

Image Credits

Tags: #Java #Spring #Sprint Security #Login #JWT #Basic Auth

Newsletter

ABMELDEN