Привет Хабр! При реализации Angular проекта, остро встал вопрос о безопасности graphql запросов в Angular 4. Выбор пал на JSON Web Tokens. Это открытый стандарт по RFC 7519.
Работает JWT по следующей схеме:

Я начал изучать программирование и Angular относительно недавно, полгода назад, и являюсь наивным чукотским малъчиком. Поэтому любую критику относительно кода и логики, приму как дружеский совет.
Клиентом graphql мы использовали apollo-angular (docs, github), и токены JWT нужны были в хедере каждого запроса к GraphQL API.
Создаем наш сервис авторизации AuthService. Первичное получение токена реализовано через REST:
Получаем accessToken и пишем его в sessionStorage браузера.
Далее, нам необходим клиент, для работы с API. Используем apollo-client:
В этом куске кода, мы берем наш токен из sessionStorage, и пишем его в хедер authorization.
У apollo-client есть пара методов для networkInterface: Middleware и Afterware. В нашем случае, использовался Middleware, его целью является применение тех или иных параметров, перед отправкой запроса к API.
И еще один немаловажный момент. В параметре opts, указан mode: 'cors'. Это сделано для Spring Security, на котором крутится мой бэк, в случае если на бекэнде нет cross-origin HTTP request фильтра, мод можно переключить на 'no-cors'.
Теперь, все запросы или мутации, уходящие посредством apollo-client, будут иметь в хедере наш jwt-токен. На бэкенде, реализована проверка этого токена на валидность и жизнеспособность по времени. Код не мой.
Теперь о refreshToken. Задача refreshToken'a обновить устаревший accessToken.
Реализации могут быть разные, начиная от примитивной проверки в AuthGuard сервисе Angular'a, заканчивая scheduler сервисом, который будет обновлять токен по заданному интервалу времени. В моем случае, был сделан первый вариант. Когда додумаюсь до более умного варианта, реализую. Пока что смог только так.
Итак, создаем метод в нашем сервисе AuthService, который будет вызываться, если наш сервис проверки AuthGuard заметит, что истек срок действия токена:
Далее, создаем собственно сервис проверки AuthGuard:
Здесь используется библиотека angular2-jwt и ее метод isTokenExpired(). Если метод возвращает true, вызываем созданный ранее метод refresh() и обновляем токен.
Если кому то будет интересно почитать насчет JWT, то вот хороший обзор на английском What is a JSON Web Token?
Рад критике и хорошим советам.
Работает JWT по следующей схеме:

Я начал изучать программирование и Angular относительно недавно, полгода назад, и являюсь наивным чукотским малъчиком. Поэтому любую критику относительно кода и логики, приму как дружеский совет.
Клиентом graphql мы использовали apollo-angular (docs, github), и токены JWT нужны были в хедере каждого запроса к GraphQL API.
Создаем наш сервис авторизации AuthService. Первичное получение токена реализовано через 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; } }) };
Получаем accessToken и пишем его в sessionStorage браузера.
Здесь стоит сделать отступление и заметить, что sessionStorage живет до закрытия вкладки/браузера, и если пользователь закрыл его, то сбрасывается все содержимое, и как следствие, теряется токен. Альтернатива: localStorage или cookies. В этом случае, токен будет находиться у пользователя до ручного удаления.Есть еще refreshToken. О нем чуть позже.
Однако тут есть свои подводные камни. Какие именно камни, можно прочитать в этой статье.
Далее, нам необходим клиент, для работы с API. Используем 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{}
В этом куске кода, мы берем наш токен из sessionStorage, и пишем его в хедер authorization.
У apollo-client есть пара методов для networkInterface: Middleware и Afterware. В нашем случае, использовался Middleware, его целью является применение тех или иных параметров, перед отправкой запроса к API.
И еще один немаловажный момент. В параметре opts, указан mode: 'cors'. Это сделано для Spring Security, на котором крутится мой бэк, в случае если на бекэнде нет cross-origin HTTP request фильтра, мод можно переключить на 'no-cors'.
Теперь, все запросы или мутации, уходящие посредством apollo-client, будут иметь в хедере наш jwt-токен. На бэкенде, реализована проверка этого токена на валидность и жизнеспособность по времени. Код не мой.
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; }
Теперь о refreshToken. Задача refreshToken'a обновить устаревший accessToken.
Реализации могут быть разные, начиная от примитивной проверки в AuthGuard сервисе Angular'a, заканчивая scheduler сервисом, который будет обновлять токен по заданному интервалу времени. В моем случае, был сделан первый вариант. Когда додумаюсь до более умного варианта, реализую. Пока что смог только так.
Итак, создаем метод в нашем сервисе AuthService, который будет вызываться, если наш сервис проверки AuthGuard заметит, что истек срок действия токена:
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'); } }
Далее, создаем собственно сервис проверки AuthGuard:
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'); } }
Здесь используется библиотека angular2-jwt и ее метод isTokenExpired(). Если метод возвращает true, вызываем созданный ранее метод refresh() и обновляем токен.
Если кому то будет интересно почитать насчет JWT, то вот хороший обзор на английском What is a JSON Web Token?
Рад критике и хорошим советам.
