Skip to content

Latest commit

 

History

History
315 lines (240 loc) · 10.8 KB

README.md

File metadata and controls

315 lines (240 loc) · 10.8 KB

jwt-server

Validation of JSON Web tokens on resource servers. Components:

  • spring - Spring Boot integration (of the above) via Spring Security.

Spring Boot Integration

Spring Boot starter for Json Web Token (JWT) authorization.

Functional overview:

  • Get Json Web Keys from the Authorization Server; use them to
  • verify JWT signatures in incoming requests, then
  • enforce generic JWT claim constraints (like checking audience, timestamp etc), then
  • parse authorities (permissions) and populate the security context,
  • let Spring Security enforce access per method (@EnableGlobalMethodSecurity(prePostEnabled = true)
  • optionally injects transformed JWT security context into downstream method signatures

Also supports a few typical CORS settings. A custom WebSecurityConfigurerAdapter can be provided by the application.

Configuration

Tenant configuration

The default implementation supports configuration of one or more tenants.

  • tenant id (key for configuration)
  • issuer
  • JSON Web Key location

Claim validation (common for all tenants) can also be added:

  • audiences
  • expires-at leeway
  • issued-at leeway
  • and more

For YAML, this amounts to something like

entur:
  jwt:
    enabled: true # note: false is the default value
    tenants:
      myKeycloak: # i.e. tenant id
        issuer: https://myRealm.keycloak.com
        jwk: 
          location: https://my.keycloak.com/auth/realms/myRealm/protocol/openid-connect/certs
    claims:
      expires-at-leeway: 15
      issued-at-leeway: 5
      audiences: # at least one
        - https://my.audience

Authorization configuration

By default, all requests must be so-called fully authenticated. In other words all requests must have a valid JWT token (from any of the configured tenants).

Open endpoints (i.e. permitted for all, open to the world) must be explicitly configured (white-listed) using MVC or Ant matchers.

Example for open endpoints:

entur:
  authorization:
    enabled: true # note: this is the default value
    permit-all:
      matcher:
        patterns:
          - /unprotected/**
      matcher:
        patterns:
         - /some/path/{myVariable}

Note that Spring Web uses MVC matchers. In other words, for a @RestController with a method

@GetMapping(value = "/open/country/{countryCode}")
public String authenticatedEndpoint(){
    // your code here
}

add the MVC matcher /open/country/{countryCode}. Optionally also specify the HTTP method using

entur:
  authorization:
    permit-all:
      matcher:
        method:	
          get:
            patterns:
              - /some/path/{myVariable}

MVC matchers are in general broader than Ant matchers:

  • antMatchers("/unprotected") matches only the exact /unprotected URL
  • mvcMatchers("/unprotected") matches /unprotected as well as /unprotected/, /unprotected.html, /unprotected.xyz

Also note that if the JWT filter knows there is no permit-all (while entur.authorization.enabled=true), it will reject requests without JWT earlier in the chain.

Implementation note: The fully authenticated check will take effect before request unmarshalling and thus before interaction with closed REST controller endpoints. So unwanted requests (i.e. requests without a token which obviously will fail @PreAuthorize authorization checks in the controller) are rejected as early as possible, in a secure and lightweight way.

Actuator

To expose actuator endpoints , add

entur:
  authorization:
    permit-all:
      matcher:
        patterns:
          - /actuator/**

Adding fine-grained security to your Controller

Secure endpoints using method access-control expressions by adding the @PreAuthorize and @PostAuthorize annotations. See the following code example to see a basic implementation.

@RestController
public class TestController {

    @GetMapping(value = "/myAdminService",  produces = arrayOf(MediaType.TEXT_PLAIN_VALUE))
    @PreAuthorize("hasAnyAuthority('admin')")
    public String authenticatedEndpoint(){
        // your code here
    }
}

See the Spring documentation for further details.

Accessing JWT claims

The library also makes it possible to inject a derived method argument generated from the authorization header:

@RestController
public class TestController {

    @GetMapping(value = "/myAdminService",  produces = arrayOf(MediaType.TEXT_PLAIN_VALUE))
    @PreAuthorize("hasAnyAuthority('admin')")
    public String authenticatedEndpoint(JwtPayload token){
        // your code here
    }
}

See configuration of JwtArgumentResolver for further details. Alternatively get to the token via the security context:

JwtAuthenticationToken authentication = (JwtAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();

// get token value (including Bearer)
String token = authentication.getCredentials(); //  'Bearer XYZ'

// get claim
String myClaim = authentication.getClaim("myClaim", String.class);

Note that 'storing a lot of stuff' in the access-token is generally not recommended.

Testing

It is higly recommended that you have some simple tests that validates that the security configuration is working as expected.

The jwt-junit5-spring module provide JUnit5 support. In a nutshell:

@AuthorizationServer
public class MyTest {

    @Test
    public void test(@AccessToken String token) throws IOException {
        // set token in Autorization header and invoke calls
    }
}

The @AuthorizationServer test extention spawns/overwrites the tenant JWK location with a local file URL.

Multi-tentant testing

Use tenant id for multi-tentant support:

@AuthorizationServer("myKeycloak")
@AuthorizationServer("myAuth0")
public class MyTest {

    @Test
    public void test(@AccessToken(by = "myKeycloak") String token) throws IOException {
        // set token in Autorization header and invoke calls
    }
}

See jwt-test for further details.

Json Web Keys (JWK) configuration

As a reminder; JWKs are the keys used to validate JWT signatures, so the data inside can be trusted. They are provided by the Authorization Server.

The default configuration is as follows:

  • refresh cache every 60 minutes
  • block at most 15 seconds for cache refresh before failing
  • instantly retry once to capture transient IOExceptions
  • keep cache at most 10 hours, should cache refresh fail (i.e. outage)
  • limit refreshes (triggered by unknown signature keys) to at most 1 per 10 seconds
  • Spring HealthIndicator enabled

This corresponds to the following configuration:

entur:
    jwt:
        jwk:
            cache:
              enabled: true
              time-to-live: 3600 # seconds
              refresh-timeout: 15 # seconds
              preemptive:
                time-to-expires: 15 # seconds
            retry:
              enabled: true
            outage-cache:
              enabled: true
              time-to-live: 36000 #seconds
            rate-limit: # per tenant
              enabled: true
              refill-rate: 0.1 # per second

Note that eager cache refresh (if enabled) does not kick off until first time the cache is populated, which would normally be through health checks or first time a token is to be validated.

Key rotation

The authorization server might choose to rotate its keys. When that happens, the key id used to sign the access-tokens changes, and a new key id will be noted in the JWT header. This implementation will promptly refresh the keys and normally be able to instantly verify new tokens. Previously issued tokens will be able to verify if the key list still contains the old key.

Health indicator

The library supports a Spring HealthIndicator via the enabling jwks health indicator. It looks at the last attempt to get signing keys. It will trigger a refresh if

  • no previous attempt was made
  • last attempt was unsuccessful

In other words, the health check will not refresh expired keys, but repeated calls to the health-check will result in a positive result once downstream services are back up. As a positive side-effect, on startup, calling the health-check before opening for traffic will result in the cache being populated (read: warmed up).

Context logging

For copying interesting JWT fields through to the MDC logging context, configure mappings:

entur:
  jwt:
    mdc:
      enabled: true
      mappings:
      - from: iss #from claim
        to: issuer

Cross-Origin Resource Sharing (CORS)

The CORS support is intended for use-cases where your customers do NOT normally call your API directly from web-browser and other distributed apps like mobile applications (technically, this cannot be prevented using CORS). With the notable exception of your development portal.

This is so that API-keys, client credentials, access-tokens and/or other secrets are not compromised. Webapps are intended to have a dedicated backend service on the same domain, so no CORS request are necessary.

Configuration example:

entur:
  cors: 
    enabled: true
    mode: xyz
    origins:
      - https://myapp.entur.org
      - https://myotherpetstore.swagger.io

where xyz is from the following list

  • api - CORS requests from selected sites. Recommended exceptions:
    • Your API development portal
    • Petstore
  • webapp - no CORS requests are accepted

If no mode is set, no configuration is added by this starter. This allows for adding your own custom implementation.

@Bean("corsConfigurationSource")
public CorsConfigurationSource myCorsConfigurationSource() {
    // ...
} 

Note that the bean name must be as above in order for Spring to pick up the bean.

CORS and API gateway

In general, the API gateway should respond with HTTP 403 to requests with unknown origins. All other requests, including OPTIONS calls, can be sent backwards to the Spring application.

See jwt-spring-auth0-web for a concrete implementation example.