📜 ⬆️ ⬇️

Angular - Implementing secure queries to the GraphQL API via JWT tokens

Hi Habr! When implementing the Angular project, the question of the safety of graphql queries in Angular 4 was acute. The choice fell on JSON Web Tokens . This is an open standard for RFC 7519 .

JWT works as follows:
image

I started learning programming and Angular relatively recently, six months ago, and am a naive Chukchi little boy. Therefore, any criticism regarding code and logic will be accepted as friendly advice.

We used apollo-angular ( docs , github ) as client graphql, and JWT tokens were needed in the header of each request to the GraphQL API.
')
We create our authorization service AuthService . Primary receipt of a token is implemented via REST:

login(username: string, password: string){ let headers = new Headers({ "content-type": "application/json;charset=utf-8"}); let options = new RequestOptions({ headers: headers }); return this.http.post('http://localhost:8080/login', ({ username: username, password: password }), options) .map((res : any) => { if (res.status === 200) { this.commonToken = res.json(); let data = this.commonToken; this.accessToken = JSON.stringify(data.accessToken); this.refreshToken = JSON.stringify(data.refreshToken); sessionStorage.setItem('accessToken', this.accessToken); sessionStorage.setItem('refreshToken', this.refreshToken); return true; } }) }; 

We receive accessToken and we write it in browser sessionStorage .

Here it is worth making a digression and notice that sessionStorage lives until the tab / browser is closed, and if the user closes it, all the content is discarded, and as a result, the token is lost. Alternative: localStorage or cookies . In this case, the token will remain with the user until manual deletion.
However, there are some pitfalls. What kind of stones can be found in this article .
There is still refreshToken. About him a little later.

Next, we need a client to work with the API. Use apollo-client:

 import ApolloClient, { createNetworkInterface } from 'apollo-client'; const networkInterface = createNetworkInterface({ uri: 'http://localhost:8080/graphql', opts: { mode: 'cors' } }); networkInterface.use([ { applyMiddleware(req, next) { if (!req.options.headers) { req.options.headers = {}; } if (sessionStorage.getItem('accessToken')) { req.options.headers['authorization'] = `${JSON.parse(sessionStorage.getItem('accessToken'))}`; } next(); } } ]); const apolloClient = new ApolloClient({ networkInterface }); export function provideClient(): ApolloClient { return apolloClient; } export class GraphqlClient{} 

In this piece of code, we take our token from sessionStorage, and write it to the authorization header.
The apollo-client has a couple of methods for network Interface: Middleware and Afterware . In our case, Middleware was used, its purpose is to apply certain parameters before sending a request to the API.

And one more important moment. In the opts parameter, mode is specified : 'cors' . This is done for Spring Security, on which my backline runs, if there is no cross-origin HTTP request filter on the backend, the mod can be switched to 'no-cors'.

Now, all requests or mutations leaving through the apollo-client will have our jwt-token in the header. On the backend, this token is checked for validity and viability over time. The code is not mine.

  private TokenAuthentication processAuthentication(TokenAuthentication authentication) throws AuthenticationException { String token = authentication.getToken(); DefaultClaims claims; try { claims = (DefaultClaims) Jwts.parser().setSigningKey(DefaultTokenService.KEY).parse(token).getBody(); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { throw new AuthenticationServiceException("Invalid JWT token:", ex); } catch (ExpiredJwtException expiredEx) { throw new AuthenticationServiceException("JWT Token expired", expiredEx); } return buildFullTokenAuthentication(authentication, claims); if (claims.get("TOKEN_EXPIRATION_DATE", Long.class) == null) throw new AuthenticationServiceException("Invalid tokens"); Date expiredDate = new Date(claims.get("TOKEN_EXPIRATION_DATE", Long.class)); if (expiredDate.after(new Date())) return buildFullTokenAuthentication(authentication, claims); else throw new AuthenticationServiceException("Token expired date error"); } private TokenAuthentication buildFullTokenAuthentication(TokenAuthentication authentication, DefaultClaims claims) { String username = claims.get("username", String.class); Long userId = Long.valueOf(claims.get("userId", String.class)); String auth = claims.get("authorities", String.class); if(Roles.REFRESH_TOKEN == auth) { throw new AuthenticationServiceException("Refresh token can't be used for authorization!!!"); } List<SimpleGrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(auth)); TokenAuthentication fullTokenAuthentication = new TokenAuthentication(authentication.getToken(), true, authorities, username, userId); return fullTokenAuthentication; } 

Now about refreshToken. The refreshToken task is to update the outdated accessToken.

Implementations can be different, starting from a primitive check in the Angular AuthGuard service, ending with a scheduler service that will update the token at a given time interval. In my case, the first option was made. When I think of a more intelligent option, I implement it. So far, I could only this way.

So, we create a method in our AuthService service, which will be called if our AuthGuard verification service notices that the token has expired:

 refresh() { let token = sessionStorage.getItem('accessToken'); let refToken = sessionStorage.getItem('refreshToken'); let headers = new Headers({ "content-type": "application/x-www-form-urlencoded"}); let options = new RequestOptions({headers: headers}); let body = new URLSearchParams(); body.set('RefreshToken', refToken); if (token != null && refToken != null) { return this.http.post('http://localhost:8080/login/refresh', body, options) .subscribe((res : any) => { if (res) { this.commonToken = res.json(); let data = this.commonToken; this.accessToken = JSON.stringify(data.accessToken); sessionStorage.setItem('accessToken', this.accessToken); } }) } else { console.error('An error occurred'); } } 

Next, create the actual AuthGuard verification service :

 import { Injectable } from '@angular/core'; import {Router, CanActivate, RouterStateSnapshot, ActivatedRouteSnapshot} from '@angular/router'; import {JwtHelper} from "angular2-jwt"; import {AuthService} from "./auth.service"; @Injectable() export class AuthGuard implements CanActivate { jwtHelper: JwtHelper = new JwtHelper(); constructor(private authService: AuthService, private router: Router) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { let accessToken = sessionStorage.getItem('accessToken'); let refreshToken = sessionStorage.getItem('refreshToken'); if (accessToken && refreshToken) { if (this.jwtHelper.isTokenExpired(accessToken)){ this.authService.refresh() } else { return true } } this.router.navigateByUrl('/unauthorized'); } } 

It uses the angular2-jwt library and its isTokenExpired () method. If the method returns true, call the previously created refresh () method and update the token.

If anyone is interested in reading about JWT, then here is a good overview in English. What is a JSON Web Token?

Glad to criticism and good advice.

Source: https://habr.com/ru/post/336082/


All Articles