/// /// Copyright © 2016-2019 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// import {Injectable, NgZone} from '@angular/core'; import {JwtHelperService} from '@auth0/angular-jwt'; import {HttpClient} from '@angular/common/http'; import {combineLatest, forkJoin, Observable, of} from 'rxjs'; import {distinctUntilChanged, filter, map, skip, tap} from 'rxjs/operators'; import {LoginRequest, LoginResponse} from '../../shared/models/login.models'; import {ActivatedRoute, Router, UrlTree} from '@angular/router'; import {defaultHttpOptions} from '../http/http-utils'; import {ReplaySubject} from 'rxjs/internal/ReplaySubject'; import {UserService} from '../http/user.service'; import {select, Store} from '@ngrx/store'; import {AppState} from '../core.state'; import {ActionAuthAuthenticated, ActionAuthLoadUser, ActionAuthUnauthenticated} from './auth.actions'; import {getCurrentAuthUser, selectIsAuthenticated, selectIsUserLoaded} from './auth.selectors'; import {Authority} from '../../shared/models/authority.enum'; import {ActionSettingsChangeLanguage} from '@app/core/settings/settings.actions'; import {AuthPayload} from '@core/auth/auth.models'; import {TranslateService} from '@ngx-translate/core'; import {AuthUser} from '@shared/models/user.model'; import {TimeService} from '@core/services/time.service'; @Injectable({ providedIn: 'root' }) export class AuthService { constructor( private store: Store, private http: HttpClient, private userService: UserService, private timeService: TimeService, private router: Router, private route: ActivatedRoute, private zone: NgZone, private translate: TranslateService ) { combineLatest( this.store.pipe(select(selectIsAuthenticated)), this.store.pipe(select(selectIsUserLoaded)) ).pipe( map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})), distinctUntilChanged(), filter((data) => data.isUserLoaded ), skip(1), ).subscribe((data) => { this.gotoDefaultPlace(data.isAuthenticated); }); this.reloadUser(); } redirectUrl: string; private refreshTokenSubject: ReplaySubject = null; private jwtHelper = new JwtHelperService(); private static _storeGet(key) { return localStorage.getItem(key); } private static isTokenValid(prefix) { const clientExpiration = AuthService._storeGet(prefix + '_expiration'); return clientExpiration && Number(clientExpiration) > (new Date().valueOf() + 2000); } public static isJwtTokenValid() { return AuthService.isTokenValid('jwt_token'); } private static clearTokenData() { localStorage.removeItem('jwt_token'); localStorage.removeItem('jwt_token_expiration'); localStorage.removeItem('refresh_token'); localStorage.removeItem('refresh_token_expiration'); } public static getJwtToken() { return AuthService._storeGet('jwt_token'); } public reloadUser() { this.loadUser(true).subscribe( (authPayload) => { this.notifyAuthenticated(authPayload); this.notifyUserLoaded(true); }, () => { this.notifyUnauthenticated(); this.notifyUserLoaded(true); } ); } public login(loginRequest: LoginRequest): Observable { return this.http.post('/api/auth/login', loginRequest, defaultHttpOptions()).pipe( tap((loginResponse: LoginResponse) => { this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); } )); } public sendResetPasswordLink(email: string) { return this.http.post('/api/noauth/resetPasswordByEmail', {email}, defaultHttpOptions()); } public activate(activateToken: string, password: string): Observable { return this.http.post('/api/noauth/activate', {activateToken, password}, defaultHttpOptions()).pipe( tap((loginResponse: LoginResponse) => { this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); } )); } public resetPassword(resetToken: string, password: string): Observable { return this.http.post('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()).pipe( tap((loginResponse: LoginResponse) => { this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); } )); } public changePassword(currentPassword: string, newPassword: string) { return this.http.post('/api/auth/changePassword', {currentPassword, newPassword}, defaultHttpOptions()); } public activateByEmailCode(emailCode: string): Observable { return this.http.post(`/api/noauth/activateByEmailCode?emailCode=${emailCode}`, null, defaultHttpOptions()); } public resendEmailActivation(email: string) { return this.http.post(`/api/noauth/resendEmailActivation?email=${email}`, null, defaultHttpOptions()); } public loginAsUser(userId: string) { return this.http.get(`/api/user/${userId}/token`, defaultHttpOptions()).pipe( tap((loginResponse: LoginResponse) => { this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); } )); } public logout(captureLastUrl: boolean = false) { if (captureLastUrl) { this.redirectUrl = this.router.url; } this.http.post('/api/auth/logout', null, defaultHttpOptions(true, true)) .subscribe(() => { this.clearJwtToken(); }, () => { this.clearJwtToken(); } ); } private notifyUserLoaded(isUserLoaded: boolean) { this.store.dispatch(new ActionAuthLoadUser({isUserLoaded})); } public gotoDefaultPlace(isAuthenticated: boolean) { const url = this.defaultUrl(isAuthenticated); this.zone.run(() => { this.router.navigateByUrl(url); }); } public defaultUrl(isAuthenticated: boolean): UrlTree { if (isAuthenticated) { if (this.redirectUrl) { const redirectUrl = this.redirectUrl; this.redirectUrl = null; return this.router.parseUrl(redirectUrl); } else { // TODO: return this.router.parseUrl('home'); } } else { return this.router.parseUrl('login'); } } private loadUser(doTokenRefresh): Observable { const authUser = getCurrentAuthUser(this.store); if (!authUser) { return this.procceedJwtTokenValidate(doTokenRefresh); } else { return of({} as AuthPayload); } } private procceedJwtTokenValidate(doTokenRefresh: boolean): Observable { const loadUserSubject = new ReplaySubject(); this.validateJwtToken(doTokenRefresh).subscribe( () => { let authPayload = {} as AuthPayload; const jwtToken = AuthService._storeGet('jwt_token'); authPayload.authUser = this.jwtHelper.decodeToken(jwtToken); if (authPayload.authUser && authPayload.authUser.scopes && authPayload.authUser.scopes.length) { authPayload.authUser.authority = Authority[authPayload.authUser.scopes[0]]; } else if (authPayload.authUser) { authPayload.authUser.authority = Authority.ANONYMOUS; } const sysParamsObservable = this.loadSystemParams(authPayload.authUser); if (authPayload.authUser.isPublic) { // TODO: } else if (authPayload.authUser.userId) { this.userService.getUser(authPayload.authUser.userId).subscribe( (user) => { sysParamsObservable.subscribe( (sysParams) => { authPayload = {...authPayload, ...sysParams}; authPayload.userDetails = user; let userLang; if (authPayload.userDetails.additionalInfo && authPayload.userDetails.additionalInfo.lang) { userLang = authPayload.userDetails.additionalInfo.lang; } else { userLang = null; } this.notifyUserLang(userLang); loadUserSubject.next(authPayload); loadUserSubject.complete(); }, (err) => { loadUserSubject.error(err); this.logout(); }); }, (err) => { loadUserSubject.error(err); this.logout(); } ); } else { loadUserSubject.error(null); } }, (err) => { loadUserSubject.error(err); } ); return loadUserSubject; } private loadIsUserTokenAccessEnabled(authUser: AuthUser): Observable { if (authUser.authority === Authority.SYS_ADMIN || authUser.authority === Authority.TENANT_ADMIN) { return this.http.get('/api/user/tokenAccessEnabled', defaultHttpOptions()); } else { return of(false); } } private loadSystemParams(authUser: AuthUser): Observable { const sources: Array> = [this.loadIsUserTokenAccessEnabled(authUser), this.timeService.loadMaxDatapointsLimit()]; return forkJoin(sources) .pipe(map((data) => { const userTokenAccessEnabled: boolean = data[0]; return {userTokenAccessEnabled}; })); } public refreshJwtToken(): Observable { let response: Observable = this.refreshTokenSubject; if (this.refreshTokenSubject === null) { this.refreshTokenSubject = new ReplaySubject(1); response = this.refreshTokenSubject; const refreshToken = AuthService._storeGet('refresh_token'); const refreshTokenValid = AuthService.isTokenValid('refresh_token'); this.setUserFromJwtToken(null, null, false); if (!refreshTokenValid) { this.refreshTokenSubject.error(new Error(this.translate.instant('access.refresh-token-expired'))); this.refreshTokenSubject = null; } else { const refreshTokenRequest = { refreshToken }; const refreshObservable = this.http.post('/api/auth/token', refreshTokenRequest, defaultHttpOptions()); refreshObservable.subscribe((loginResponse: LoginResponse) => { this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, false); this.refreshTokenSubject.next(loginResponse); this.refreshTokenSubject.complete(); this.refreshTokenSubject = null; }, () => { this.clearJwtToken(); this.refreshTokenSubject.error(new Error(this.translate.instant('access.refresh-token-failed'))); this.refreshTokenSubject = null; }); } } return response; } private validateJwtToken(doRefresh): Observable { const subject = new ReplaySubject(); if (!AuthService.isTokenValid('jwt_token')) { if (doRefresh) { this.refreshJwtToken().subscribe( () => { subject.next(); subject.complete(); }, (err) => { subject.error(err); } ); } else { this.clearJwtToken(); subject.error(null); } } else { subject.next(); subject.complete(); } return subject; } public refreshTokenPending() { return this.refreshTokenSubject !== null; } public setUserFromJwtToken(jwtToken, refreshToken, notify) { if (!jwtToken) { AuthService.clearTokenData(); if (notify) { this.notifyUnauthenticated(); } } else { this.updateAndValidateToken(jwtToken, 'jwt_token', true); this.updateAndValidateToken(refreshToken, 'refresh_token', true); if (notify) { this.notifyUserLoaded(false); this.loadUser(false).subscribe( (authPayload) => { this.notifyUserLoaded(true); this.notifyAuthenticated(authPayload); }, () => { this.notifyUserLoaded(true); this.notifyUnauthenticated(); } ); } else { this.loadUser(false); } } } private notifyUnauthenticated() { this.store.dispatch(new ActionAuthUnauthenticated()); } private notifyAuthenticated(authPayload: AuthPayload) { this.store.dispatch(new ActionAuthAuthenticated(authPayload)); } private notifyUserLang(userLang: string) { this.store.dispatch(new ActionSettingsChangeLanguage({userLang})); } private updateAndValidateToken(token, prefix, notify) { let valid = false; const tokenData = this.jwtHelper.decodeToken(token); const issuedAt = tokenData.iat; const expTime = tokenData.exp; if (issuedAt && expTime) { const ttl = expTime - issuedAt; if (ttl > 0) { const clientExpiration = new Date().valueOf() + ttl * 1000; localStorage.setItem(prefix, token); localStorage.setItem(prefix + '_expiration', '' + clientExpiration); valid = true; } } if (!valid && notify) { this.notifyUnauthenticated(); } } private clearJwtToken() { this.setUserFromJwtToken(null, null, true); } }