Using Angular and Reactive Spring With JWT Tokens
Want to learn more about using Angular and the reactive Spring Boot framework? Check out this tutorial on how to use Angular and Spring with JWT tokens.
Join the DZone community and get the full member experience.
Join For FreeThe project AngularAndSpring provides an example of using Angular and reactive Spring secured with JWT tokens. The AngularAndSpring project can use the embedded Mongo DB for easy, local testing. For the topic of reactive programming in Angular and Spring, here is an article for more details.
What Do Angular and Spring Do?
This is an application that shows the quotes of cryptocurrencies with four types of exchanges. The currencies have detailed pages with daily/weekly/monthly charts and a report with the data on the chart. After the login the orderbook page can be used, the orderbooks can be seen. The orderbook data is requested from the exchanges, and, because of that, the page needs to be secured.
JWT Tokens
JWT Tokens are used because they can secure the access to REST interfaces without a session between the browser and the server. The token is base64-encoded and signed by the server. The browser logs in with a username and password and gets a token as a response. The token is then stored in the browser and, then, is sent in the HTTP header in all secured requests to the server. The server can check the signature and the expiration and, then, provide access to the REST API, making the API stateless and secure.
Configuration
The security configuration of Spring Boot is in the WebSecurityConfig:
@Configuration
@Order(SecurityProperties.DEFAULT_FILTER_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationProvider authProvider;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().anyRequest().permitAll().anyRequest().anonymous();
http.antMatcher("/**/orderbook").authorizeRequests().anyRequest().authenticated();
http.csrf().disable();
http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider));
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
}
Line 14 sets the session creation policy to stateless for the JWT Tokens.
Line 15 sets the default permissions to anonymous to provide access without tokens.
Line 16 sets the permissions for the orderbook REST API to be authenticated to use the tokens.
Line 18 sets the JwtTokenFilterConfigurer with the JwtTokenProvider.
JWT Token Providers
The JwtTokenProviders class creates and processes the JWT Tokens:
@Component
public class JwtTokenProvider {
@Value("${security.jwt.token.secret-key}")
private String secretKey;
@Value("${security.jwt.token.expire-length}")
private long validityInMilliseconds; // 24h
@Autowired
private ReactiveMongoOperations operations;
public String createToken(String username, List<Role> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority()))
.filter(Objects::nonNull).collect(Collectors.toList()));
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
String encodedSecretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
return Jwts.builder().setClaims(claims).setIssuedAt(now).setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, encodedSecretKey).compact();
}
public Optional<Jws<Claims>> getClaims(Optional<String> token) {
if (!token.isPresent()) {
return Optional.empty();
}
String encodedSecretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
return Optional.of(Jwts.parser().setSigningKey(encodedSecretKey).parseClaimsJws(token.get()));
}
public Authentication getAuthentication(String token) {
Query query = new Query();
query.addCriteria(Criteria.where("userId").is(getUsername(token)));
MyUser user = operations.findOne(query, MyUser.class).block();
return new UsernamePasswordAuthenticationToken(user, "", user.getAuthorities());
}
public String getUsername(String token) {
String encodedSecretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
return Jwts.parser().setSigningKey(encodedSecretKey).parseClaimsJws(token).getBody().getSubject();
}
public String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
public boolean validateToken(String token) {
String encodedSecretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
try {
Jwts.parser().setSigningKey(encodedSecretKey).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new RuntimeException("Expired or invalid JWT token");
}
}
}
Lines 13-24 create a JWT Token for the username and roles provided. The claims of the token are the roles of Spring Boot. The expiration is set by the validityInMilliseconds
that is set in the application.properties. The key to signing the JWT Token is set by the secretKey
that is also set in the application.properties and needs to be base64-encoded.
Lines 26-32 encode the secretKey
and read the claims of the token.
Lines 34-40 create a UsernamePasswordToken
based on the values of the user in the Mongo DB for the authentication in the filter chain.
Lines 42-45 read the username out of the JWT Token.
Lines 47-53 read the JWT Token out of the HTTP header and returns the token string.
Lines 55-63 validate the token with the encoded secretKey
. That makes sure that the token has not been tampered with.
Login Process
The login is done with the MyUserController that provides the login API:
@PostMapping("/login")
public Mono<MyUser> postUserLogin(@RequestBody MyUser myUser,HttpServletRequest request)
throws NoSuchAlgorithmException, InvalidKeySpecException {
Query query = new Query();
query.addCriteria(Criteria.where("userId").is(myUser.getUserId()));
return this.operations.findOne(query, MyUser.class).switchIfEmpty(Mono.just(new MyUser()))
.map(user1 -> loginHelp(user1, myUser.getPassword()));
}
private MyUser loginHelp(MyUser user, String passwd) {
if (user.getUserId() != null) {
String encryptedPassword;
try {
encryptedPassword = this.passwordEncryption.getEncryptedPassword(passwd, user.getSalt());
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
return new MyUser();
}
if (user.getPassword().equals(encryptedPassword)) {
String jwtToken = this.jwtTokenProvider.createToken(user.getUserId(), Arrays.asList(Role.USERS));
user.setToken(jwtToken);
user.setPassword("XXX");
return user;
}
}
return new MyUser();
}
In Lines 1-8, the API call for the login is provided. The user has to post a MyUser
that has userId, salt, password. Then, the user object for the userId
is retrieved from the Mongo DB and mapped to the new MyUser
object.
In Lines 10-26, the password that the user provided is encrypted and checked against the password in the DB. If the password is ok the Jwt token is created and added to the MyUser object for the user and the password is removed. Then the user gets the MyUser object with the Jwt Token back.
Angular Login Service
The login service myuser.service takes care of the Angular side of the login:
@Injectable()
export class MyuserService {
private _reqOptionsArgs= { headers: new HttpHeaders().set( 'Content-Type', 'application/json' ) };
private _utils = new Utils();
private myUserUrl = "/myuser";
constructor(private http: HttpClient, private pl: PlatformLocation ) {
}
postLogin(user: MyUser): Observable<MyUser> {
return this.http.post<MyUser>(this.myUserUrl+'/login', user, this._reqOptionsArgs).pipe(map(res => {
let retval = <MyUser>res;
localStorage.setItem("salt", retval.salt);
localStorage.setItem("token", retval.token);
return retval;
}),catchError(this._utils.handleError<MyUser>('postLogin')));
}
In Lines 10-16, the user object with the username and password is sent to the server and the response is processed. The salt and the token of the response is put in LocalStorage
to be used for the orderbook requests.
Angular Orderbook Request
The request for one of the orderbooks is done in bitfinex.service:
getOrderbook(currencypair: string): Observable<OrderbookBf> {
let reqOptions = {headers: this._utils.createTokenHeader()};
return this.http.get<OrderbookBf>(this._bitfinex+'/'+currencypair+'/orderbook/', reqOptions).pipe(catchError(this._utils.handleError<OrderbookBf>('getOrderbook')));
}
Here, a normal get request is sent to the server with a custom header in thereqOptions
.
The following token is added in the utils:
export class Utils {
get token():string {
return !localStorage.getItem("token") ? null : localStorage.getItem("token");
}
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;
}
In Lines 3-5, a getter is created that provides the token out of the LocalStorage
or null.
In Lines 7-13, the HTTP header is built. If the token exists in LocalStorage,
the authorization with the bearer and token are added.
Summary
The project AngularAndSpring shows that Angular, Spring Boot, Spring Security and JWT Tokens can work together. The backend is stateless and makes it horizontally scalable. The anonymous part of the application uses the reactive features of Spring Boot 2. The frontend uses Angular with Material and has charts, animations, and supports multiple languages. It can be started with the in-memory Mongo DB and now will be tested standalone.
Opinions expressed by DZone contributors are their own.
Comments