be.looorent:spring-security-jwt

Create stateless security on your Spring endpoints using JWT.


License
MIT

Documentation

Build Status Maven Central

Spring JWT Authentication

This JAR is relevant to you if:

  • Your application's users are authenticated using Json Web Token.
  • Your application's endpoints must be secured base on these tokens.
  • These endpoints are developed using Spring Boot.

Disclaimer

This JAR is originally developed for my own needs. Do not hesitate to extend it.

Requirements

  • JDK 1.8
  • Spring Boot 1.4
  • Servlet 3.1

Choices

  • All requests must be authenticated except if they match authentication.publicRoute.

Getting started

1) Add the JAR to your classpath

Add the JAR to the classpath will enable JWT security automatically if your Spring Boot application enables auto-configurations (i.e. @EnableAutoConfiguration`). It is available on Maven Central. To do so, for instance:

  • with Gradle:
compile "be.looorent:spring-security-jwt:0.7"
  • or with Maven:
<dependency>
    <groupId>be.looorent</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>0.7</version>
</dependency>

2) Define the authentication configuration

In your properties, (e.g. application.yml), 3 properties must be defined:

  • authentication.tokenIssuer: The JWT issuer, which is check beside the private key. Type: String
  • authentication.tokenSecretKey: The JWT secret key. This key must not be Base64-encoded. Type: String
  • authentication.publicRoute: Ant pattern for routes that do not require a valid token. Do not mind what HTTP method is used. Type: String

For instance, in a YAML file using environment properties:

authentication:
  tokenIssuer: ${TOKEN_ISSUER}
  tokenSecretKey: ${TOKEN_SECRET_KEY}
  publicRoute: /open/**

3) Define CORS

This JAR also define a CORS filter on top of each request that is made to your Spring application. In your properties, (e.g. application.yml), 4 properties must be defined:

  • http.headers.allowedOrigins: All allowed origins (e.g. a client web application domain). This values can be Java regular expressions (this feature is provided by a custom implementation be.looorent.security.jwt.RegexCorsConfiguration) Type: List<String>
  • http.headers.allowedMethods: All allowed HTTP methods. Type: List<String>
  • http.headers.allowedHeaders: List of headers that a pre-flight request can list as allowed for use during an actual request. Authorization must always be present in this list. Type: List<String>
  • http.headers.cacheMaxAge: Configure how long, in seconds, the response from a pre-flight request can be cached by clients. Type: Long

For instance, in a YAML file:

http:
  headers:
    allowedOrigins:
      - https://your-web-app.io
    allowedMethods:
      - POST
      - PUT
      - GET
      - OPTIONS
      - DELETE
    allowedHeaders:
      - Access-Control-Allow-Headers
      - Origin
      - Accept
      - Authorization
      - X-Requested-With
      - Content-Type
      - Access-Control-Request-Method
      - Access-Control-Request-Headers
    cacheMaxAge: 3600

4) Provide an implementation of UserDetailsFactory

In order to let you handle your String UserDetails (structure, permissions, granted authorities, ...), An implementation of UserDetailsFactory must be provided. This UserDetails will be added to the Security Context of each authenticated request.

This implementation MUST BE registered as a Spring Bean.

For instance: a Java class can implement this interface to find a User (defined in your own codebase) from the database.

@Service
class UserPrincipalFactoryImpl implements UserDetailsFactory {

    private static final String FACEBOOK_ID_KEY = "facebookId";

    private final UserRepository userRepository;

    @Autowired
    UserPrincipalFactoryImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails createFrom(Claims tokenClaims, HttpServletRequest request) throws UserDoesNotExistException {
        User user;
        if (isForUserCreation(request)) {
            user = createUserFromBody(request);
        }
        else {
            user = findUser(tokenClaims);
        }
        return new UserPrincipal(tokenClaims, user);
    }

    private User createUserFromBody(HttpServletRequest request) {
        ...
    }

    private boolean isForUserCreation(HttpServletRequest request) {
        ...
    }

    private User findUser(Claims claims) {
        return ofNullable(userRepository.findByXXX(claims.get("XXX")))
                .orElseThrow(() -> new UserDoesNotExistException("User does not exists for this XXX"));
    }
}

Where UserPrincipal is your own UserDetails implementation (which, in this example, contains the user retrieved from database).

5) Define a HandlerMethodArgumentResolver to always get the current user (Optional)

If you wan to inject your User object into your controllers' methods, you can provide an implementation of String's HandlerMethodArgumentResolver. See http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/method/support/HandlerMethodArgumentResolver.html

Here you are!

How to disable this configuration

If your Spring application enables auto-configurations, this security configuration will be enabled by default. To disable it, exclude JwtSecurityAutoConfiguration from auto-configurations.

@EnableAutoConfiguration(exclude=[JwtSecurityAutoConfiguration])
@SpringBootApplication
class YourApplicationMainClass {
    ...
}

Error handling

Status code

These error HTTP statuses can be returned for each authenticated request:

  • 412 when the user referenced by the token does not exist (see the exception of type UserDoesNotExistException that can be returned by your own implementation). In this situation, an additional header response Authentication-User-Does-Not-Exist is set to true.
  • 401 when the token is refused. The reason is written in the response body. These reasons are:
    • jws_unsupported_by_application : when receiving a JWT in a particular format/configuration that does not match the format expected by the application.
    • jws_malformed : indicates that a JWT was not correctly constructed and should be rejected.
    • jwt_expired : indicates that a JWT was accepted after it expired and must be rejected.
    • jwt_wrong_signature : indicates that either calculating a signature or verifying an existing signature of a JWT failed.
    • jwt_missing_bearer_token: indicates that no Bearer Token has been provided through the Authorization header.
    • Another unexpected message
  • 403 if another authentication error occurs

Response format

The body response is structured as followed:

{
    "reason": "XXX"
}

How to deploy a new version to Maven central

Following this great article, you should configure your ./gradle/gradle.propreties file and then:

$ gradle -Prelease uploadArchives closeAndPromoteRepository

Future work

  • More tests
  • More documentation
  • Public key support