FC
FACADE
CODE
Lesson 22 of 28
FC
FACADE
CODE

Learn Spring Security

Contents

01. Bootstrap The Application
1. Introduction
2. Install Spring Security
3. Enable Basic Auth
4. Authentication with AppUser
5. Password Encoder
02. Web Layer Security - RBAC
6. Permit Public APIs
7. Role Based Authorization
8. Disable CSRF
9. Current Authenticated User
03. Web Layer Security - PBAC
10. Permission Based Authorization
11. Define Permissions
12. Assign Permissions
13. Remove Role Based Access
04. Service Layer Security
14. PreAuthorize
15. PostAuthorize
16. Authorize Using Spring Beans
05. Domain Object Instance Security
17. Domain Object Instance Security
18. PermissionEvaluator Interface
19. PermissionEvaluator Strategy
20. DB Backed UserDetailsService
06. Token Based Authentication
21. Basic Authentication Revisited
22. Generate Token
23. Persist Token
24. Verify Token
25. Invalidate Token
07. Token Based Authentication and Authorization
26. JSON Web Token
27. Generate JWT
28. Verify JWT

Generate Token

Tokens are always better than username/password as they are uniquely generated on each login. And they do not hang around for a long time as they are invalidated either when the user logout or when it reaches its expiry time.

Token-based auth requires the user to provide username/password only once during login. It then generates a unique token (generally a random string) for the user on successful authentication. This unique token can be sent on subsequent requests to identify the user until the token expires or invalidated.

But using tokens requires a bit of learning curve on how to generate, verify and invalidate them which is not the case with Basic Auth. We will implement token-based authentication mechanism in this section using our own token of random string i.e., UUID.

Note

JSON Web Token (JWT) is an open, industry standard token specification widely popular for many reasons. Rather than focusing on the type of the token first, let's understand how token-based authentication mechanism works. And then we will replace our token with JWT using battle-tested production grade JWT library.

AuthenticationService

First we will create an AuthenticationService component to implement generateToken() method which accepts username and password and returns UUID on successful authentication.


@Service
public class AuthenticationService {
    public String generateToken(String username, String password) {
        // Authenticate using Spring Security's AuthenticationManager with username/password
        // Generate and return token if authentication success
    }
}

AuthenticationManager

We are not going to perform authentication by ourselves by checking the existence of username and password in the database. We will use AuthentionManager, we talked about in Current Authenticated User chapter, where it accepts Authentication object as authentication request and returns the same as authenticated principal on successful authentication.

We will use UsernamePasswordAuthenticationToken to create an Authentication object as authentication request using username & password provided by the user. AuthenticationManager delegates the authentication process to a list of AuthenticationProvider. DaoAuthenticationProvider is one of such implementations we discussed in DB Backed UserDetailsService chapter, which uses our DbUserDetailsService to load the user details by username. It will then perform the authentication and return the authenticated principal. We will then return the UUID token after verifying the authenticated principal.


@Service
public class AuthenticationService {
    @Autowired
    private AuthenticationManager authenticationManager;

    public String generateToken(String username, String password) {
        Authentication authentication = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
        authentication = authenticationManager.authenticate(authentication);

        String token = null;
        if (authentication.isAuthenticated()) {
            token = UUID.randomUUID().toString();
        }

        return token;
    }
}

By default AuthenticationManager is not exposed as a bean, we have to retrieve it from AuthenticationConfiguration and expose it as a bean from SecurityConfig as below:


@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {
    // Other details omitted for brevity

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

AuthenticationController

Now let's create a REST API to get the username and password from the user and return the generated token back from AuthenticationService.


@RestController
@RequestMapping("auth")
public class AuthenticationController {
    @Autowired
    private AuthenticationService authenticationService;

    @PostMapping(path = "token", consumes = {APPLICATION_FORM_URLENCODED_VALUE})
    public ResponseEntity<Map<String,String>> generateToken(@RequestParam("username") String username,
                                @RequestParam("password") String password) {
        String token = authenticationService.generateToken(username, password);
        Map<String, String> tokenResponse = Collections.singletonMap("token", token);
        return ResponseEntity.ok(tokenResponse);
    }
}

As GenerateToken API must be accessible to everyone, we have to update HttpSecurity to permit this API. So we will add the API url in SecurityConstants as well.


@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .sessionManagement(
            httpSecuritySessionManagementConfigurer ->
                    httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .authorizeRequests(auth -> auth
            .antMatchers(POST, API_AUTH_TOKEN).permitAll()
            .antMatchers(GET, PUBLIC_API_LIST).permitAll()
            .anyRequest().authenticated()
        );
    return http.build();
}

Token Generation

Submit a POST request to GenerateToken API with Admin user credentials in x-www-form-urlencoded body type like below, we will get the UUID token returned after successful authentication.

UUID token generated for Admin user credentials on successful authentication
UUID token generated for Admin user credentials on successful authentication

Please note that we have removed httpBasic() in HttpSecurity configuration as we are replacing it with token-based authentication. This will cause all the protected APIs to be inaccessible even for the authorized users. And this is because Spring Security can not identify the user at this stage, as we are yet to verify the token and tell Spring Security who the user is and what are they allowed to do.

But before verifying the token we have to associate the token with the user who requested it by storing it in a persistant datastore to verify it on each requests.