Client for obtaining Json Web Token (JWT) using so-called client credentials.
Features:
- caching with pro-active refreshes
- minimizes thread congestion
- reduces spikes in processing time
- minimizes number of calls to authorization servers
- fault-tolerance / robustness
- transient network error protection (retry once)
- health status (on last remote access)
Essentially this allows a single JWT to be shared within a single JVM, drastically reducing the load on Authorization Servers, and reducing the server response time.
Supported servers:
- Auth0
- Keycloak
See submodules for Maven / Gradle coordinates.
The cache by default behaves in a lazy, proactive way:
- if missing or expired token, the first thread to access the cache requests a new token, while all other threads must wait (are blocked, with a deadline).
- if a token is about to expire, request a new in a background thread, while returning the (still valid) current token.
So while empty or expired tokens means that the implementation is essentially blocking, this is preferable to letting all thread request the same token from the authorization server.
Since we're refreshing the cache before the token expires, there will normally not be both a lot of traffic and an expired cache; so thread spaghetti (by blocking) should be avoided.
Eager refresh can also be enabled.
Create an instance of AccessTokenProvider
per application-context (per Authorization Server). Instances of AccessTokenProvider
cache og refresh access-tokens and are thread-safe. Example:
ClientCredentials credentials = Auth0ClientCredentialsBuilder.newInstance()
.withHost("my.auth0.com")
.withProtocol("https")
.withSecret("mySecret")
.withClientId("myClientID")
.build();
// relate network timeouts to cache configuration, see further down
long connectTimeout = 10000;
long readTimeout = 10000;
AccessTokenProvider accessTokenProvider = AccessTokenProviderBuilder.newBuilder(credentials, connectTimeout, readTimeout).build();
Store the accessTokenProvider
in your application context, then get an access token:
AccessToken accessToken = accessTokenProvider.getAccessToken(false); // or true for force refresh token
String jwt = accessToken.getValue(); // on the form x.y.z
// do remote requests (your code here)
Spring Boot starter for configuration of AccessTokenProvider
beans.
entur:
jwt:
clients:
auth0:
myClient:
audience: myAudience
client-id: myClientId
host: entur.org
scope: myScope
secret: mySecret
For audiences/APIs using permissions, scopes can be used to limit the permissions included in the token (see example).
Most machine-to-machine clients should not need to specify scope, and rather run with the default assigned permissions (if any).
entur:
jwt:
clients:
keycloak:
myClient:
audience: myAudience
client-id: myClientId
host: entur.org
realm: myTenant
scope: myScope
secret: mySecret
Configure additional clients by adding keys under entur.jwt.clients.keycloak
and entur.jwt.clients.auth0
(like myClient
) in above examples, i.e.
entur:
jwt:
clients:
keycloak:
myFirstClient:
audience: myFirstAudience
client-id: myFirstClientId
host: first.entur.org
realm: myFirstTenant
scope: myFirstScope
secret: myFirstSecret
mySecondClient:
audience: mySecondAudience
client-id: mySecondClientId
host: second.entur.org
realm: mySecondTenant
scope: mySecondScope
secret: mySecondSecret
Then use a Qualifier
to autowire:
@Autowired
@Qualifier("myFirstClient")
private AccessTokenProvider firstAccessTokenProvider;
@Autowired
@Qualifier("mySecondClient")
private AccessTokenProvider secondAccessTokenProvider;
To mock AccessTokenProvider
use either @MockBean
or override the bean by id (i.e. myFirstClient
and/or mySecondClient
in the above examples).
To adjust the caching / validity of the token, add the following properties (in addition to the above ones):
entur:
jwt:
clients:
auth0:
myClient:
retrying: true # retry (once) if getting token fails
cache:
minimum-time-to-live: 15 # minimum time left on token (seconds)
refresh-timeout: 15 # timeout when refreshing the token (seconds)
preemptive-refresh:
time-to-expires: 30 # time left on token when refreshing in the background (seconds)
eager:
enabled: false
Note that
- proper configuration of
minimum-time-to-live
depends on the so-called expiryleeway
configure in the remote service you'll be calling using the access-token. - eager preemptive-refresh is triggered by the
ContextStartedEvent
event, which Spring applications must implicitly invoke.start()
to publish. For example using
SpringApplication.run(MyApplication.class, args).start();
Those wanting to preemptivly refresh tokens long before they expire, i.e. to always have some minutes (or hours) left on tokens in case of authorization server downtime,
please note the preemptive-refresh parameter expires-constraint
, which limits how often preemptive refresh happens (as a percentage of the access-token lifetime).
This prevents adjustments of the access-token time-to-live (which can be dynamically configured on the authorization server) from resulting in constant refreshes, which could negatively affect the authorization server.
The library supports a Spring HealthIndicator via the enabling jwts
health indicator.
The health indicator looks at the last attempt to get credentials. 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 tokens, 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).