Angular JWT Autorefresh With Spring Boot
Join the DZone community and get the full member experience.
Join For FreeIf JWTs are used for security, there are use cases for using short-lived tokens — for example, if you use the token in a URL that gets opened. To make that possible, the Angular frontend needs to refresh the token before it expires automatically. To make that possible, Angular and RxJs features are used in the frontend and a Spring Boot REST endpoint checks and updates the JWT in the backend.
The frontend design is based on the concepts explained in this article. It is a valuable resource about caching with RxJs.
In this article, the AngularPortfolioMgr project is used to provide examples. The project is currently a work in progress, but the feature shown in this article is done.
The Angular Frontend
The AngularPortfolioMgr project has a signin/login process that receives the JWT and starts the autoupdates of the tokens. The login.component.ts gets the token.service.ts injected and sets the token in the login method in the service:
xxxxxxxxxx
private login(login: Login): void {
if (login && login.token && login.id) {
this.tokenService.token = login.token;
this.tokenService.userId = login.id;
this.data.login = login;
this.loginFailed = false;
this.dialogRef.close(this.data.login);
} else {
this.loginFailed = true;
}
}
On line 1, we check that the login was successful. On line 2, the token is set in the TokenService setter that starts the autoupdate of the tokens. On line 3, the userId is set in the TokenService setter. On line 7, the login dialog gets closed.
TokenService
The token.service.ts is used to manage and refresh the tokens received in the login process. It requests a new token every 45 seconds and provides with a getter/setter and an Observable for the token. The Subscriber of the Observable has to take care to unsubscribe properly. The Service is set up like this:
xxxxxxxxxx
@Injectable({
providedIn: 'root'
})
export class TokenService {
private myTokenCache: Observable<RefreshTokenResponse>;
private readonly CACHE_SIZE = 1;
private readonly REFRESH_INTERVAL = 45000; //45 sec
private myToken: string;
private myUserId: number;
private myTokenSubscription: Subscription;
private stopTimer: Subject<boolean>;
constructor(private http: HttpClient, private router: Router) { }
In lines 1-3, is it created in 'root' to have one the application.
In line 5, the Observable to cache the RefreshTokenResponse interfaces with the token is defined.
In line 6, the cache size is set to 1 because we only need 1 token.
In line 7, the refresh interval is set to 45 seconds.
In line 8, the current value of the token gets defined.
In line 10, the subscription to the to the cache gets defined.
In line 11, the stopTimer Subject gets defined to stop the refresh pipe.
In line 12, the HttpClient and the Router gets injected by Angular to request the token updates and routes the user to the login if the updates fail.
The token setter sets the initial token value and starts the autorefesh of the token:
xxxxxxxxxx
set token(token: string) {
this.myToken = token;
if(token && !this.myTokenCache) {
const myStopTimer = new Subject<boolean>();
this.stopTimer = myStopTimer;
const myTimer = timer(0, this.REFRESH_INTERVAL);
this.myTokenCache = myTimer.pipe(
takeUntil(myStopTimer),
switchMap(() => this.refreshToken()),
shareReplay(this.CACHE_SIZE));
this.myTokenSubscription = this.myTokenCache.subscribe(newToken =>
this.myToken = newToken.refreshToken, () => this.clear());
}
}
In line 2, the new token is set in the service property. In line 3, it is checked if the tokenCache needs to be initialized.
In line 4-5, the myStopTime Subject gets created to stop the pipe when the Subject emits, and it is set to the property. In line 6, the timer is set up to tick every 45 seconds.
In line 7, the timer is connected to a pipe and the token cache Oberservable. In line 8, the takeUntil stops the pipe when the myStopTimer emits something.
In line 9, switchMap gets used to update the token cache. In line 10, shareReplay is used to to provide a cache for all the subscribers of the token cache Observable.
In line 11, the token Subscription gets set up and set to be able to unsubscribe. In line 12, the myToken property gets updated with the subscription and in the error case the clear() method gets called.
The refreshToken method requests a new token from the backend.
xxxxxxxxxx
private refreshToken(): Observable<RefreshTokenResponse> {
return this.http.get<RefreshTokenResponse>('/rest/auth/refreshToken', {
headers: this.createTokenHeader()
});
}
This is a normal Angular service call where the header parameters need to be set.
The createTokenHeader method creates the needed headers for the TokenService and the token.interceptor.ts.
xxxxxxxxxx
public createTokenHeader(): HttpHeaders {
let reqOptions = new HttpHeaders().set('Content-Type','application/json')
if(this.token) {
reqOptions = new HttpHeaders().set( 'Content-Type', 'application/json' ).set('Authorization', `Bearer ${this.token}`);
}
return reqOptions;
}
The createTokenHeader method adds the 'Authorization' header param if the token is availiable. It is also used in the token.interceptor.ts to add the header to all other service requests if the user is logged in.
The clear method cleans up the service after a failed token refresh request or a user logout.
xxxxxxxxxx
public clear() {
if(this.myTokenSubscription) {
this.myTokenSubscription.unsubscribe();
}
if(this.stopTimer) {
this.stopTimer.next(true);
this.stopTimer = null;
}
this.myTokenCache = null;
this.myToken = null;
this.myUserId = null;
this.router.navigate(['/login']);
}
In lines 2-4, the subscription that refreshes the local token value is unsubscribed. In lines 5-8, the stopTimes emits a value to stop the refresh pipe and the property is reset.
In lines 9-11, the service properties are reset. That includes the timer(myTokenCache). In line 12, the user gets routed to the login component. The main.route.guard.ts will prevent routing to other routes by checking for the token in the TokenService.
The Spring Boot Backend
The backend provides a REST endpoint to check/update the token send. It uses a Spring Reactor, but the code can be easily used in any Spring application.
xxxxxxxxxx
"/rest/auth") (
public class AuthenticationController {
...
private AppUserService appUserService;
...
"/refreshToken") (
public Mono<RefreshTokenDto> getRefreshToken( (value =
HttpHeaders.AUTHORIZATION) String bearerStr) {
return this.appUserService.refreshToken(bearerStr);
}
}
In lines 1-3, a Spring REST controller AuthenticationController.java is defined with the base mapping to '/rest/auth'.
In lines 5-6, the AppUserService gets injected.
In lines 8-11, the rest endpoint '/refreshToken' gets defined and the header paramter of the Authorization is injected in the bearerStr variable. Then the refreshToken method gets called and the result gets returned in the RefreshTokenDto.java.
In the AppUserService.java, the token string gets checked and updated.
xxxxxxxxxx
public Mono<RefreshTokenDto> refreshToken(String bearerToken) {
Optional<String> tokenOpt = this.jwtTokenProvider.resolveToken(bearerToken);
if(tokenOpt.isEmpty()) {
throw new AuthorizationServiceException("Invalid token");
}
String newToken = this.jwtTokenProvider.refreshToken(tokenOpt.get());
LOGGER.info("Jwt Token refreshed.");
return Mono.just(new RefreshTokenDto(newToken));
}
In lines 2-5, the bearer part of the token is stripped off. If not, an exception is throw. In line 6, the JwtTokenProvider.java is used to check/refresh the token.
In line 8, the new token String is put in the RefreshTokenDto.java and returned.
In the JwtTokenProvider.java, the token string gets checked, updated, and signed.
public String refreshToken(String token) {
validateToken(token);
Optional<Jws<Claims>> claimsOpt = this.getClaims(Optional.of(token));
if(claimsOpt.isEmpty()) {
throw new AuthorizationServiceException("Invalid token claims");
}
Claims claims = claimsOpt.get().getBody();
claims.setIssuedAt(new Date());
claims.setExpiration(new Date(Instant.now().toEpochMilli() +
validityInMilliseconds));
String newToken =
Jwts.builder().setClaims(claims).signWith(this.jwtTokenKey, SignatureAlgorithm.HS256).compact();
return newToken;
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(this.jwtTokenKey)
.build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new AuthenticationException("Expired or invalid JWT token",e);
}
}
In line 2, the validateToken method is called. It checks that the token is a token, the signature is correct, and the token is not expired.
In lines 3-6, the claims of the token are retrieved and checked. In lines 7-10, the claims are updated to the new times. In lines 11-13, a new token string is created and signed.
Conclusion
Adding the auto-refresh token feature to the Angular frontend required surprisingly little code. The capabilities of Angular and RxJs were a great help. To have community resources like this article saves a lot of time. Angular, with its community, is a great opportunity for anybody with a Java background.
Spring Boot makes creating the backed easy and provides the opportunity to use reactive programming from frontend to the backend.
Opinions expressed by DZone contributors are their own.
Comments