Skip to content

Java backend application using Spring-security to implement JWT based Authentication and Authorization

License

Notifications You must be signed in to change notification settings

hardikSinghBehl/jwt-auth-flow-spring-security

Repository files navigation

JWT Authentication and Authorization Flow using Spring Security

A reference proof-of-concept that leverages Spring-security to implement JWT based authentication, API access control, Token revocation and Compromised password detection.
🛠 upgraded to Spring Boot 3 and Spring Security 6 🛠

Key Components

Any request to a secured endpoint is intercepted by the JwtAuthenticationFilter, which is added to the security filter chain and configured in the SecurityConfiguration. The custom filter holds the responsibility for verifying the authenticity of the incoming access token and populating the security context.

Public API declaration

Any API that needs to be made public can be annotated with @PublicEndpoint. Requests to the configured API paths will not evaluated by the custom security filter with the logic being governed by ApiEndpointSecurityInspector.

Below is a sample controller method declared as public which will be exempted from authentication checks:

@PublicEndpoint
@GetMapping(value = "/api/v1/something")
public ResponseEntity<Something> getSomething() {
    var something = someService.fetch();
    return ResponseEntity.ok(something);
}

Token Generation and Configuration

The application uses Access Tokens (JWT) and Refresh Tokens, both of which are returned to the client upon successful authentication. JWTs are signed and verified using RS512 asymmetric key pair, wherein a private key (PKCS#8 format) is used for signing and the corresponding public key is used for verification whenever a private endpoint is invoked, with these operations handled by JwtUtility. Refresh tokens are random 256-bit values generated by RefreshTokenGenerator and stored in a cache against the user identifier by AuthenticationService.

Token validity/expiration (In minutes) and the asymmetric key pairs can be configured in the active .yml file. The configured values are populated in TokenConfigurationProperties and referenced by the application. Below is a sample snippet.

com:
  behl:
    cerberus:
      token:
        access-token:
          private-key: ${JWT_PRIVATE_KEY}
          public-key: ${JWT_PUBLIC_KEY}
          validity: 30
        refresh-token:
          validity: 120

API Access Control

Access control is imposed by the application based on the user's current status within the system. The corresponding permissions are embedded into the generated JWT which allows for stateless access control and authorization process.

For detailed explanation, this Document can be referenced.

Token Revocation

When a user's status is demoted to one with fewer privileges, there is a need to invalidate the existing Access Token since it retains the previous enhanced scopes. The implementation leverages the JWT Token Identifier (JTI) and a caching mechanism to achieve this. Incoming Bearer tokens are validated by the JwtAuthenticationFilter to ensure they've not been revoked. By revoking access tokens promptly, the system maintains the integrity of access control.

For detailed explanation, this Document can be referenced.

Authentication Failure

Spring security exceptions are commenced at the AuthenticationEntryPoint. A custom implementation, CustomAuthenticationEntryPoint is configured in SecurityConfiguration which assumes any exceptions thrown by the authentication filters are due to token verification failure. Hence, the implementation instantiates TokenVerificationException and delegates the responsibility of exception handling to HandlerExceptionResolver. The exception finally gets evaluated by ExceptionResponseHandler and approprate exception response is returned to the client.

The below API response is returned by the application in the event of a token verification failure during security evaluation.

{
  "Status": "401 UNAUTHORIZED",
  "Description": "Authentication failure: Token missing, invalid, revoked or expired"
}

The below API response is returned when authentication succeeds i.e Access Token is validated and Spring context is populated successfully, However the token does not have the permission required to access the API.

{
  "Status": "403 FORBIDDEN",
  "Description": "Access Denied: You do not have sufficient privileges to access this resource."
}

If the user's permissions have changed, the client can leverage available refresh token to request a new JWT, reflecting the new permissions that the user has obtained.

Compromised Password Detection

To protect user accounts from the use of vulnerable passwords that have been exposed in data breaches, the project uses the new compromised password detection feature added in spring-security:6.3. The default implementation provided uses the Have I Been Pwned API under the hood.

The compromised password check is performed during two key scenarios:

  • User creation: When a new user is being registered via the user creation API, the provided password is checked.
  • User Login: Even if a password was not compromised at the time of user creation, it may become compromised at a later point. To address this, the login API also incorporates the compromised password check.

In case of compromised password detection in any of the above scenarios, the server responds with the below error:

{
  "Status": "422 UNPROCESSABLE_ENTITY",
  "Description": "The provided password is compromised and cannot be used for account creation."
}

To recover from a compromised password situation during login, a new API endpoint PUT /users/reset-password is exposed. This endpoint accepts the below request body payload:

{
  "EmailId": "[email protected]",
  "CurrentPassword": "somethingCompromised",
  "NewPassword": "somethingSecured"
}

The new password is also checked for compromise before allowing the password reset.


Local Setup

The below given commands can be executed in the project's base directory to build an image and start required container(s). Docker compose will initiate a MySQL and Redis container as well, with the backend swagger-ui accessible at http://localhost:8080/swagger-ui.html

JWT_PRIVATE_KEY=$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048)
JWT_PUBLIC_KEY=$(echo "$JWT_PRIVATE_KEY" | openssl rsa -pubout -outform PEM)
sudo docker-compose build
sudo JWT_PRIVATE_KEY="$JWT_PRIVATE_KEY" JWT_PUBLIC_KEY="$JWT_PUBLIC_KEY" docker-compose up -d

The above commands also generate an RSA key pair locally and pass them as environment variables when starting Docker Compose, so that the application can make use of the generated key pair for signing and verifying JWTs.

To remove the environment variables from memory after the application has started, the below commands can be executed

unset JWT_PRIVATE_KEY
unset JWT_PUBLIC_KEY

Visual Walkthrough

jwt-auth-flow.mov