Spring Boot authentication with Angular 8 using NGXS+ JWT+Http Only Cookie+Spring Session JDBC

Whenever we talk about web development, our biggest concern goes to the security of our application.How can we protect our resources ?and how can we protect the users informations ?.
In this article we are going to implement a web application using spring boot as a back end framework and angular as a front end.
We are going to use spring security with jwt and http only cookies.
A good pratice is to use two tokens : access token(short duration) and refresh token(long duration) , if the client has an invalid access token and a valid refresh token the acces token will be re-created.
Also, we are not going to store our tokens in local storage beacause it’s vulnerable to XSS attacks and can be easily stolen , instead we will store them encrypted in http only cookies,
these cookies are set in the api server and sent in the http header with each request. And that way our tokens will be safe.
this schema will give us a glimps of how this will work:

One more thing , we will have one session for each user , so that the same user can’t log in from different browsers.

Now let’s take a look at our pom.xml and see what dependencies we need:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <scope>test</scope>
</dependency>
<dependency>
<! - Automated JSON API documentation for API's built with Spring → <groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId> <version>2.8.0</version> </dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId> <version>2.8.0</version>
</dependency>
<! - security → <dependency> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency>
</dependencies>

application.properties : where we will keep token and session configurations:

spring.jpa.hibernate.ddl-auto = create-drop
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jackson.serialization.fail-on-empty-beans=false
server.port=8080
spring.main.allow-bean-definition-overriding=true
spring.datasource.url= jdbc:postgresql://localhost:5432/dbtest
spring.datasource.username=postgres
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
authentication-test.auth.accessTokenCookieName=AuthToken
authentication-test.auth.refreshTokenCookieName=RefreshToken
sessionTimePki.app.jwtExpiration=5400000
#authentication-test.auth.refreshTokenExpirationMsec=7776000000
authentication-test.auth.refreshTokenExpirationMsec=10800000
app.jwtSecret=jwtAliSecretKey
#always
spring.session.jdbc.initialize-schema=always
spring.session.store-type=jdbc
spring.session.jdbc.schema=classpath:org/springframework/session/jdbc/schema-@@platform@@.sql
spring.session.jdbc.table-name=SPRING_SESSION
server.servlet.session.cookie.http-only=true
server.servlet.session.timeout=20m

SecurityConfig.java , We will keep the spring boot standard configuration and we will add the specific configuration for our session management:

@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().exceptionHandling().and().authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated().and()
.formLogin().disable().httpBasic().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry()).and()
.sessionFixation().migrateSession();


http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}@Bean
public SessionRegistry sessionRegistry() {
SessionRegistry sessionRegistry = new SessionRegistryImpl();
return sessionRegistry;
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}

JwtAuthenticationFilter: Interception the request and and check if there is a valid token in the cookie:

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtToken(httpServletRequest, true);
if (StringUtils.hasText(jwt) && jwtTokenUtil.validateToken(jwt)) {
String username = jwtTokenUtil.getUsername(jwt);
UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
else if(!jwtTokenUtil.validateToken(jwt)){
httpServletResponse.addHeader("Message","Invalid Token");
}
} catch (Exception ex) {
ex.printStackTrace();
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
String accessToken = bearerToken.substring(7);
if (accessToken == null) return null;
return SecurityCipher.decrypt(accessToken);
}
return null;
}

private String getJwtFromCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if(cookies==null)
return "";
for (Cookie cookie : cookies) {
if (accessTokenCookieName.equals(cookie.getName())) {
String accessToken = cookie.getValue();
if (accessToken == null) return null;
return SecurityCipher.decrypt(accessToken);
}
}
return null;
}

private String getJwtToken(HttpServletRequest request, boolean fromCookie) {
if (fromCookie) return getJwtFromCookie(request);
return getJwtFromRequest(request);
}

JwtTokenProvider: this is where we are going to create our tokens

public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
System.out.println("Invalid JWT Signature");
} catch (MalformedJwtException ex) {
System.out.println("Invalid JWT token");
} catch (ExpiredJwtException ex) {
System.out.println("Expired JWT token");
} catch (UnsupportedJwtException ex) {
System.out.println("Unsupported JWT exception");
} catch (IllegalArgumentException ex) {
System.out.println("Jwt claims string is empty");
}
return false;
}
public Token generateToken(User user ) {Claims claims = Jwts.claims().setSubject(user.getUsername());claims.put("auth", user.getRoles().stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList()));Date now = new Date();
Long duration = now.getTime() + jwtExpirationInMs;
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.HOUR_OF_DAY, 8);
String token = Jwts.builder().setClaims(claims).setSubject((user.getUsername())).setIssuedAt(new Date())
.setExpiration(expiryDate).signWith(SignatureAlgorithm.HS256, jwtSecret).compact();

return new Token(Token.TokenType.ACCESS, token, duration, LocalDateTime.ofInstant(expiryDate.toInstant(), ZoneId.systemDefault()));
}public Token generateRefreshToken(User user) {Claims claims = Jwts.claims().setSubject(user.getUsername());claims.put("auth", user.getRoles().stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList()));
Date now = new Date();
Long duration = now.getTime() + refreshTokenExpirationMsec;
Date expiryDate = new Date(now.getTime() + refreshTokenExpirationMsec);
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.HOUR_OF_DAY, 8);
String token = Jwts.builder().setClaims(claims).setSubject((user.getUsername())).setIssuedAt(new Date())
.setExpiration(expiryDate).signWith(SignatureAlgorithm.HS256, jwtSecret).compact();

return new Token(Token.TokenType.REFRESH, token, duration, LocalDateTime.ofInstant(expiryDate.toInstant(), ZoneId.systemDefault()));
}

CookieUtil: create http cookies

public HttpCookie createAccessTokenCookie(String token, Long duration) {
String encryptedToken = SecurityCipher.encrypt(token);
return ResponseCookie.from(accessTokenCookieName, encryptedToken)
.maxAge(duration)
.httpOnly(true)
.path("/")
.build();
}
public HttpCookie createRefreshTokenCookie(String token, Long duration) {
String encryptedToken = SecurityCipher.encrypt(token);
return ResponseCookie.from(refreshTokenCookieName, encryptedToken)
.maxAge(duration)
.httpOnly(true)
.path("/")
.build();
}

AuthController: controller for authentification , refresh and logout

@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
FindByIndexNameSessionRepository sessionRepository;
@PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<LoginResponse> login(
@CookieValue(name = "accessToken", required = false) String accessToken,
@CookieValue(name = "refreshToken", required = false) String refreshToken,
@Valid @RequestBody LoginRequest loginRequest
) {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
if(isAlreadyLoggedIn(loginRequest.getUsername())){
LoginResponse loginResponse = new LoginResponse();
loginResponse.setError("User Already logged in");
return ResponseEntity.ok(loginResponse);
}
SecurityContextHolder.getContext().setAuthentication(authentication);
String decryptedAccessToken = SecurityCipher.decrypt(accessToken);
String decryptedRefreshToken = SecurityCipher.decrypt(refreshToken);
return userService.login(loginRequest, decryptedAccessToken, decryptedRefreshToken);
}
@PostMapping(value = "/refresh", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<LoginResponse> refreshToken(@CookieValue(name = "accessToken", required = false) String accessToken,
@CookieValue(name = "refreshToken", required = false) String refreshToken) {
String decryptedAccessToken = SecurityCipher.decrypt(accessToken);
String decryptedRefreshToken = SecurityCipher.decrypt(refreshToken);
return userService.refresh(decryptedAccessToken, decryptedRefreshToken);
}
@GetMapping("/logout")
public ResponseEntity<String> logOut(HttpServletRequest request, HttpServletResponse response){
return new ResponseEntity (new ApiResponseMessage(true, userService.logout(request, response)), HttpStatus.OK);}
private Boolean isAlreadyLoggedIn(String pricipalName) {
Map user = sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,pricipalName);
return user.size()>0;
}
}

Full structure of our backend:

Back end structure

Now as have finished the back end , let’s move to the client side , as we said before we are going to use angular , in this tutorial we are going to focus on the
intercetor beacuase it’s where most of the work will be done.
Like we said in the beginning we are not going to use local storage to store our tokens , so there is no way to decode the token from cookie and read the user’s information because the token is encrypted in http only cookie.
So we need some kind of store to save the current user.
We are going to use Ngxs store which is a global state manager that dispatches actions and provides a way to select data slices out from the global state.

we need 3 dependencies in order to use NGXS:

npm install @ngxs/store
npm install @ngxs/logger-plugin
npm install @ngxs/storage-plugin

And then we implement our actions and states:

action.ts
state.ts

After setting up our state we will implement our authentication service:

const URL_BASE = environment.baseUrl;@Injectable({providedIn: 'root'})
export class AuthService {
constructor(private router: Router, private store: Store,private httpClient: HttpClient) {
}signIn(request: LoginRequest):Observable<any>{return this.httpClient.post<any>(URL_BASE + '/auth/login', request, {withCredentials: true});}refresh():Observable<any>{return this.httpClient.post<any>(URL_BASE+'/auth/refresh', {withCredentials: true});}getUser(): Observable<any> {return this.httpClient.get<any>(URL_BASE+'/profile/me', {withCredentials: true}).pipe(tap(user => {console.log(user);}));;}logOut():Observable<any>{return this.httpClient.get(URL_BASE+'/auth/logout',{withCredentials: true}).pipe(tap(res => {console.log(res);this.store.dispatch(new Reset());localStorage.clear();this.router.navigate(['']);}))}}

And now the most important part is implementing the interceptor: This is where we are going to send the cookies in the header of our request and handle the server’s response.

@Injectable()export class Interceptor implements HttpInterceptor {constructor(private auth: AuthService,private  toaster: ToastrService) {}intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {const re1 = '/assets';const re2 = '/auth/logout';const re3 = '/auth/login';if (request.url.search(re1) === -1&& request.url.search(re2) === -1&& request.url.search(re3) === -1){request = request.clone({withCredentials: true});}return next.handle(request).pipe(catchError((err: any) => {if(err.status==401){// if request is unauthorizedif(err.headers.get('Message')=='Invalid token'){// if token is invalidreturn this.refreshToken(request, next);} else {// if session is expiredthis.toaster.info('Votre session à étè expirée');this.auth.logOut().subscribe(res => console.log(res),err => console.log(err))}}}));}private refreshToken(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {return this.auth.refresh().pipe(switchMap((res) => {return next.handle(this.addAuthorizationHeader(request, res.status));},));}private addAuthorizationHeader(request: HttpRequest<any>, res: string): HttpRequest<any> {if (res.toString()=='SUCCESS') {// if refresh token is validreturn request.clone({withCredentials: true});}// if refresh token is invalidthis.auth.logOut().subscribe(res => console.log(res),err => console.log(err))}}

We are going to handle 3 types kinds of errors:

  • if the server returns an error with 401 status then we should check if it’s due to invalid token if it’s the case then we will call the refresh token to get an another access token
  • if the server returns 401 status but it’s not an invalid token then it’s definetly a session expiracy so we have to logout the user directly.
  • The third option and happens rarely if the refresh token is expired , this is unlikely to happen cause our refresh token will expire after 3 months but we need to handle this situation anyways , so if the access token is expired and we call the refresh token and still get 401 unauthorized than the refresh token is definetly expired in that case we have logout the user and destroy the session.

As a final result we can see the access token , refresh token and the session stored as http only cookie.

You can find the full source code in the git repository below:

Full stack (Spring boot — Angular ) developer and computer science engineering student

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store