I've implemented a login page using Angular 2. After login, I get jsonwebtoken, userId, userRole, userName from server. I'm storing this info in localstorage so that I can access it any time and maintain login state if user refreshes page.
AuthService.ts
import {Injectable} from "#angular/core";
#Injectable()
export class AuthService {
redirectUrl: string;
logout() {
localStorage.clear();
}
isLoggedIn() {
return localStorage.getItem('token') !== null;
}
isAdmin() {
return localStorage.getItem('role') === 'admin';
}
isUser() {
return localStorage.getItem('role') === 'user';
}
}
To check the login status, I'm just checking if token exists in localstorage. As localstorage is editable so just adding any token in localstorage would bypass login page. Similarly, if client edit user role in localstorage, client can easily access admin or user pages.
How do I solve these problems?
This is more like a general problem, I want to know how websites maintain login status?
P.S.
NodeJS Server side login code to generate jsonwebtoken
const jwt = require('jsonwebtoken');
const User = require('../models/User');
/**
* POST /login
* Sign in using username and password
*/
exports.postLogin = (req, res, next) => {
User.findOne({username: req.body.username})
.then(user=> {
if (!user) {
res.status(401);
throw new Error('Invalid username');
}
return user.comparePassword(req.body.password)
.then(isMatch=> {
if (isMatch != true) {
res.status(401);
throw new Error('Invalid password');
}
let token = jwt.sign({user: user}, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_TIMEOUT
});
return res.status(200).json({
success: true,
token: token,
userId: user._id,
role:user.role,
name:user.name
});
});
})
.catch(err=>next(err));
};
-Thanks
Tokens are supposed to be unique and hard to type (as of a big length). Also, they should be refreshed with some frequency. Better to read oAuth docs on this
Roles should not be stored on client side. Only checking on server.
Also, when using oAuth consider using Scopes.
You digitally sign the authentication token on the server side:
jwt.sign({user: user}, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_TIMEOUT
})
This signature then should be verified by the server side on subsequent requests. It becomes invalid when client changes the content of the token.
store token in localStorage/sessionStorage and validate token with server whenever required. I am having following implementation to validate token
UserProfileService.ts
#Injectable()
export class UserProfileService {
private isLoggedIn: boolean = false;
private apiEndPoint: string;
constructor(private http: Http) {
this.apiEndPoint = environment.apiEndpoint;
}
login(token: string) {
localStorage.setItem('auth_token', token);
this.isLoggedIn = true;
}
logout(){
localStorage.removeItem('auth_token');
this.isLoggedIn = false;
}
isAuthorized(): Observable<boolean> {
if (!this.isLoggedIn) {
let authToken = localStorage.getItem('auth_token');
if(authToken){
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Accept', 'application/json');
headers.append('Authorization', `Bearer ${authToken}`);
return this.http.get(`${this.apiEndPoint}/validate-token`, { headers: headers })
.map(res => {
let serverResponse = res.json();
this.isLoggedIn = serverResponse['valid'];
if (!this.isLoggedIn) {
localStorage.removeItem('auth_token');
}
return this.isLoggedIn;
})
.catch(this._serverError);
}
}
return Observable.of(this.isLoggedIn);
}
private _serverError(err: any) {
localStorage.removeItem('auth_token');
if(err instanceof Response) {
console.log(err.json());
return Observable.of(false);
}
return Observable.of(false);
}
}
AuthService.ts
#Injectable()
export class CanActivateAuthGuard implements CanActivate, CanActivateChild, CanLoad {
constructor(private userProfileService: UserProfileService, private router: Router) { }
canLoad(route: Route) {
return this.userProfileService.isAuthorized().map(authorized => {
if(authorized) {
return authorized;
} else {
let url = `/${route.path}`;
this.router.navigate(['/login'], { queryParams: { redirectTo: url } });
return authorized;
}
});
}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) {
return this.userProfileService.isAuthorized().map(authorized => {
if(authorized) {
return authorized;
} else {
this.router.navigate(['/login'], { queryParams: { redirectTo: state.url } });
return authorized;
}
});
}
canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) {
return this.canActivate(route, state);
}
}
Related
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from './auth.service';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { JwtPayload } from './model/jwt-payload.model';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'secretKey',
});
}
async validate(payload: JwtPayload) {
const user = await this.authService.validateUser(payload);
if (!user) {
throw new UnauthorizedException();
}
return true;
}
}
Token is extracted from the request by PassportStrategy. I don't know how to catch the error when the token expires or gets invalid. My purpose is if there is an error because the token expired, I need to refresh the token. Otherwise do something else.
Refresh token implementation could be handled in canActivate method in custom auth guard.
If the access token is expired, the refresh token will be used to obtain a new access token. In that process, refresh token is updated too.
If both tokens aren't valid, cookies will be cleared.
#Injectable()
export class CustomAuthGuard extends AuthGuard('jwt') {
private logger = new Logger(CustomAuthGuard.name);
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
try {
const accessToken = ExtractJwt.fromExtractors([cookieExtractor])(request);
if (!accessToken)
throw new UnauthorizedException('Access token is not set');
const isValidAccessToken = this.authService.validateToken(accessToken);
if (isValidAccessToken) return this.activate(context);
const refreshToken = request.cookies[REFRESH_TOKEN_COOKIE_NAME];
if (!refreshToken)
throw new UnauthorizedException('Refresh token is not set');
const isValidRefreshToken = this.authService.validateToken(refreshToken);
if (!isValidRefreshToken)
throw new UnauthorizedException('Refresh token is not valid');
const user = await this.userService.getByRefreshToken(refreshToken);
const {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
} = this.authService.createTokens(user.id);
await this.userService.updateRefreshToken(user.id, newRefreshToken);
request.cookies[ACCESS_TOKEN_COOKIE_NAME] = newAccessToken;
request.cookies[REFRESH_TOKEN_COOKIE_NAME] = newRefreshToken;
response.cookie(ACCESS_TOKEN_COOKIE_NAME, newAccessToken, COOKIE_OPTIONS);
response.cookie(
REFRESH_TOKEN_COOKIE_NAME,
newRefreshToken,
COOKIE_OPTIONS,
);
return this.activate(context);
} catch (err) {
this.logger.error(err.message);
response.clearCookie(ACCESS_TOKEN_COOKIE_NAME, COOKIE_OPTIONS);
response.clearCookie(REFRESH_TOKEN_COOKIE_NAME, COOKIE_OPTIONS);
return false;
}
}
async activate(context: ExecutionContext): Promise<boolean> {
return super.canActivate(context) as Promise<boolean>;
}
handleRequest(err, user) {
if (err || !user) {
throw new UnauthorizedException();
}
return user;
}
}
Attaching user to the request is done in validate method in JwtStrategy class, it will be called if the access token is valid
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
readonly configService: ConfigService,
private readonly userService: UserService,
) {
super({
jwtFromRequest: cookieExtractor,
ignoreExpiration: false,
secretOrKey: configService.get('jwt.secret'),
});
}
async validate({ id }): Promise<User> {
const user = await this.userService.get(id);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Example for custom cookie extractor
export const cookieExtractor = (request: Request): string | null => {
let token = null;
if (request && request.signedCookies) {
token = request.signedCookies[ACCESS_TOKEN_COOKIE_NAME];
}
return token;
};
Instead of using the built-in AuthGuard you can create your own one and overwrite the request handler:
#Injectable()
export class MyAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info: Error) {
if (info instanceof TokenExpiredError) {
// do stuff when token is expired
console.log('token expired');
}
return user;
}
}
Depending on what you want to do, you can also overwrite the canActivate method where you have access to the request object. Have a look at the AuthGuard sourcecode.
I'm trying to refresh a JWT token after it expires, using AuthInterceptor and ErrorInterceptor, two Angular HttpInterceptor methods. I send a token via headers of AuthInterceptor request:
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const Token = this.authService.getToken(); // localStorage.getItem('token', 'provider', ...)
if (Token) {
// if token exists (logged), send custom header values with request
const authReqRepeat = req.clone({
headers: req.headers
.set('Authorization', 'Bearer ' + Token.token)
.set('X-Auth-Provider', Token.provider)
});
return next.handle(authReqRepeat);
} else {
return next.handle(req);
}
}
}
RefreshToken method is called in ErrorInterceptor, it generates a new token object that will be used in the request that failed (because token expired):
export class ErrorInterceptor implements HttpInterceptor {
constructor(private alertService: AlertService, private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(catchError((err: HttpErrorResponse) => {
if (err.status === 401) {
this.authService.logout();
} else if (err.status === 403) {
// Token expired error, so I need to generate a new token
const Token = this.authService.getToken();
return this.authService
.refreshToken(Token.refreshToken)
.pipe(flatMap((newToken) => {
// store new access token on local storage
localStorage.setItem('token', newToken.token);
localStorage.setItem('expiration', newToken.expiresIn);
// retry request with new access token generated
return next.handle(req.clone({
headers: req.headers
.set('Authorization', 'Bearer ' + newToken.token)
.set('X-Auth-Provider', Token.provider)
}));
}));
} else {
if (err.error.message) {
this.alertService.error(err.error.message);
} else {
this.alertService.error('Não foi possível completar a requisição.');
}
return throwError(err);
}
}));
}
}
I had to use HttpBackEnd to stop a infinite looping through AuthInterceptor after a new request is called by HttpErrorInterceptor:
export class AuthService {
constructor (private http: HttpClient, private handler: HttpBackend) {}
refreshToken(token: string) {
const http = new HttpClient(this.handler);
return http.post<{token: string, expiresIn: string}>(`${BACKEND_URL}/refreshtoken`, {token: token});
}
}
Node.js request:
//routes
router.post('/refreshtoken', auth, user.refreshToken);
// controllers
module.exports = async(req, res, next) => {
try {
if (req.headers['x-auth-provider'] === 'jwt') {
// refresh token
const token = req.headers.authorization.split(' ')[1];
const decodedToken = await jwt.verify(token, process.env.JWT_Key);
req.userData = {id: decodedToken.userId, email: decodedToken.email, role: decodedToken.role};
}
next();
} catch (error) {
if (error.name && error.name === 'TokenExpiredError') {
res.status(403).json({success: false, message: 'Token inválido.'});
}
res.status(401).json({success: false, message: 'Autenticação falhou.'});
}
};
exports.refreshToken = wrap(async(req, res, next) => {
const user = await User.findOne({ refresh_token: req.body.token });
if (user) {
const newToken = await jwt.sign(
{ email: user.email, userId: user._id, role: user.role },
process.env.JWT_Key,
{ expiresIn: '15m' });
const expiresAt = moment().add(900, 'second');
res.status(200).json({success: true, token: newToken, expiresIn: JSON.stringify(expiresAt.valueOf())});
} else {
res.status(401).json({success: false, message: 'Autenticação falhou.'});
}
});
It seems to work, however i messed up something because my Node.js server throws some errors: UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client.
What am I doing wrong?
I need to refresh token in HttpInterceptor before the request is made, to do it I check the access token before the request and call refresh if it's expired.
Currently, my interceptor looks like this:
#Injectable()
export class TokenInterceptor implements HttpInterceptor {
private refreshTokenSubject = new BehaviorSubject(null);
private refreshTokenObservable = this.refreshTokenSubject.asObservable();
private isRefreshingToken = false;
constructor(private authService: AuthService) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const tokenData = AuthService.getCurrentSessionData();
if (!this.isRefreshingToken) {
// if no token set - make request as-is
const tokenSet = tokenData && tokenData.token;
if (!tokenSet) {
return next.handle(request);
}
// proceed if token not expired
const tokenExpired = new Date(tokenData.expirationDate) < new Date();
if (!tokenExpired) {
return next.handle(this.setAuthHeader(request, tokenData.token));
}
// check if we can refresh the token and logout instantly if not
const tokenRefreshable = tokenData.refreshToken && new Date(tokenData.refreshTokenExpirationDate) > new Date();
if (!tokenRefreshable) {
this.authService.logout();
return Observable.throw('');
}
this.isRefreshingToken = true;
// make all subsequent requests wait for new token
this.refreshTokenSubject.next(null);
// make refresh request
return this.authService.refreshToken()
.switchMap((res: any) => {
AuthService.storeSessionData(res, Utils.getLocalStorageItem(STORAGE_KEYS.STAY_LOGGED_IN));
this.isRefreshingToken = false;
// let subsequent awaiting proceed
this.refreshTokenSubject.next(res.access_token);
return next.handle(this.setAuthHeader(request, res.access_token));
})
.catch((err) => {
this.authService.logout();
return Observable.throw('');
})
.finally(() => {
this.isRefreshingToken = false;
});
} else {
// if token refreshing in progress - wait for new token
return this.refreshTokenObservable
.filter(token => token !== null)
.take(1)
.switchMap((token) => {
return next.handle(this.setAuthHeader(request, token));
});
}
}
private setAuthHeader(request, token) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
}
}
The problem is that this.authService.refreshToken() never makes the request and subsequent requests never proceed. I guess it's because nothing subscribes to the observable returned from HttpClient, here is the refreshToken method code:
public refreshToken() {
const tokenData = AuthService.getCurrentSessionData();
return this.http.post(
`${environment.apiPath}/auth/refresh`,
{ refresh_token: tokenData.refreshToken },
);
}
How can I fix this code to make refreshToken request and let other requests proceed after it as intended?
#Injectable()
export class RequestInterceptorService implements HttpInterceptor {
#BlockUI() blockUI: NgBlockUI;
isRefreshingToken = false;
tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);
oldToken = localStorage.getItem('access_token');
constructor(private authService: AuthService, private localStorageToken: AppLocalStorage,
private route: ActivatedRoute, private router: Router) {}
addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
return req.clone({ setHeaders: { Authorization: 'Bearer ' + token, 'Access-Control-Allow-Origin': '*' }});
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse
| HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
return next.handle(this.addToken(req, this.authService.getAuthToken()))
.catch(error => {
if (error instanceof HttpErrorResponse) {
switch ((<HttpErrorResponse>error).status) {
case 400:
return this.handle400Error(error);
case 401: this.authService.refresh().subscribe((data) => { // A call has been made at an instant where network get 401 status code,It will prevent the asynchronous way of handling network calls.
this.localStorageToken.setRefreshTokens(data); //localstorageToken is used to store all the response, including access token, expiry time and refresh token to get stored in localstorage.
}
);
return this.handle401Error(req, next);
// default: this.logoutUser();
}
} else {
return Observable.throw(error);
}
});
}
handle400Error(error) {
if (error && error.status === 400 && error.error && error.error.error === 'invalid_grant') {
// If we get a 400 and the error message is 'invalid_grant', the token is no longer valid so logout.
// console.log('Bad Error');
return this.logoutUser();
}
return Observable.throw(error);
}
handle401Error(req: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshingToken) {
this.isRefreshingToken = true;
// Reset here so that the following requests wait until the token
// comes back from the refreshToken call.
this.tokenSubject.next(null);
return this.authService.refreshToken()
.switchMap((newToken: string) => {
newToken = localStorage.getItem('access_token');
if (newToken) {
this.tokenSubject.next(newToken);
if (this.oldToken === newToken) {
return this.logoutUser();
} else {
return next.handle(this.addToken(req, newToken));
}
}
// If we don't get a new token, we are in trouble so logout.
return this.logoutUser();
})
.catch(error => {
// If there is an exception calling 'refreshToken', bad news so logout.
return this.logoutUser();
})
.finally(() => {
this.isRefreshingToken = false;
});
} else {
return this.tokenSubject
.filter(token => token != null)
.take(1)
.switchMap(token => {
return next.handle(this.addToken(req, token));
});
}
}
logoutUser() {
// Route to the login page (implementation up to you)
this.router.navigate(['login']).then(() => { this.blockUI.stop(); });
return Observable.throw('');
}
}
Now the AuthService are for getting the refresh token and also for login,
This service are called whenever refresh token are needed, I gave 2 sec gap for fetching the refresh token
export class AuthService {
private TokenApi = AppSettings.DEVELOPMENT_API;
private newToken = ' ';
private current_token: string;
private refresh_token: string = localStorage.getItem('refresh_token');
constructor(private http: HttpClient, private localStorageToken: AppLocalStorage) {
}
login(username: string, password: string): Observable<TokenParams> {
const headers = new HttpHeaders({'content-type': 'application/x-www-form-urlencoded'});
const loginApi = this.TokenApi + '/oauth/token?username=' + username + '&password=' + password + '&grant_' +
'type=password....';
return this.http.post<TokenParams>(loginApi, '', {headers: headers});
}
refresh(): Observable<TokenParams> {
this.refresh_token = localStorage.getItem('refresh_token');
const refreshToken = this.TokenApi + '/oauth/token?refresh_token=' + this.refresh_token + '&grant_' +
'type=refresh_token...';
return this.http.post<TokenParams>(refreshToken, '' );
}
logout() {
this.localStorageToken.emptyLocalStorage();
}
getAuthToken() {
this.current_token = localStorage.getItem('access_token');
return this.current_token;
}
refreshToken(): Observable<string> {
this.newToken = localStorage.getItem('access_token');
this.current_token = this.newToken;
return Observable.of(localStorage.getItem('access_token')).delay(2000);
}
}
I'm working on web-app with authorization via JWT and Angular 2. I've Nodejs/express server with API and client-side on Angular2.
So, my server answers GET request correctly and gives data like this:
{
"success": true,
"user": {
"_id": "5a6ef70edb04dd29e24bb03b",
"email": "danko",
"username": "ivan"
}
}
Next, here is my auth.service.ts. Functions createAuthenticationHeaders() and getProfile() takes part in handling HTTP responses:
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders, HttpRequest, HttpResponse, HttpParams} from '#angular/common/http';
import { Http, Headers, RequestOptions} from '#angular/http'; // Http, Headers, RequestOptions
import 'rxjs/add/operator/map';
#Injectable()
export class AuthService {
domain = "http://localhost:8080";
authToken;
user;
options;
constructor(
private http: HttpClient,
private httplegacy: Http) { }
createAuthenticationHeaders() {
this.loadToken();
this.options = new RequestOptions({
headers : new Headers({
'Content-Type' : 'application/json',
'authorization' : this.authToken
})
});
}
loadToken() {
this.authToken = localStorage.getItem('token');
}
registerUser(user) {
return this.http.post(this.domain + '/authentication/register', user);
}
loginUser(user) {
return this.http.post(this.domain + '/authentication/login', user);
}
storeUserData(token, user) {
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
this.authToken = token;
this.user = user;
}
getProfile() {
this.createAuthenticationHeaders();
return this.httplegacy.get(this.domain + '/authentication/profile', this.options);
}
}
Also, here is my profile.component.ts:
import { Component, OnInit } from '#angular/core';
import { AuthService} from '../../services/auth.service';
#Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
username;
email;
constructor(
private authService: AuthService
) { }
ngOnInit() {
this.authService.getProfile().subscribe(profile => {
console.log(profile);
this.username = profile.user.username;
this.email = profile.user.email;
})
}
}
Expected behavior of these lines of code: after handling server's response with user's data with auth.service.ts(mainly createAuthenticationHeaders() and getProfile() functions), user's data is transmitted to profile.component.ts to show it on web-page using next code:
<h2 class="page-header">Profile Page</h2>
<ul class="list-group">
<li class="list-group-item">Username: {{ username }} </li>
<li class="list-group-item">Email: {{ email }}</li>
</ul>
But, while compiling I got an error: property 'user', doesn't exist on type 'Response'. Would You like to explain why i got such error, and how to fix it?
P.S.: yep, console.log(profile) gives me such info:
Response {_body: "{"success":true,"user":{"_id":"5a6ef70edb04dd29e24bb03b","email":"danko","username":"ivan"}}", status: 200, ok: true, statusText: "OK", headers: Headers, …}
headers:Headers {_headers: Map(1), _normalizedNames: Map(1)}
ok : true
status : 200
statusText : "OK"
type : 2
url : "http://localhost:8080/authentication/profile"
_body : "{"success":true,"user":{"_id":"5a6ef70edb04dd29e24bb03b","email":"danko","username":"ivan"}}"
__proto__ : Body
constructor : ƒ Response(responseOptions)
toString : ƒ ()
__proto__ :
Object
But how can I get data from _body field of response?
P.S.: code for router from server side:
router.get('/profile', (req, res) => {
User.findOne({ _id: req.decoded.userId }).select('username email').exec((err, user) => {
if (err) {
res.json({ success: false, message: err });
} else {
if(!user) {
res.json({ success: false, message: 'User not found'});
} else{
res.json({ success: true, user: user });
}
}
});
});
you try to read your data directly from the Response Object of express. You need smth like:
this.authService.getProfile().subscribe(profile => {
console.log(profile);
let p = JSON.parse(profile._body)
this.username = p.user.username;
this.email = p.user.email;
})
This will take the JSON string from the body of your HTTP Response and make it an accessible object.
NOTE:
It would be much better to tell the server to answer with a standard json due to this is web standard nowadays.
Update: #messerbill's was 50/50 correct. Such construction works:
this.authService.getProfile().subscribe(profile => {
console.log(profile);
let p = JSON.parse(profile._body)
this.username = p.user.username;
this.email = p.user.email;
})
My web-page got user's info and show it in correctly, but an error left and i've to comment these lines of code to compile and run my application, and uncomment after to see user's info on webpage.
An error message: property '_body' does not exists on type 'Response'.
So, at this moment i've no idea how it works with an error and how to create really correct structure.
Try this
ngOnInit() {
this.authService.getProfile().subscribe(profile => {
console.log(profile);
let p = profile.json();
this.username = p.user.username;
this.email = p.user.email;
})
}
I have an interceptor in Angular that I am using to refresh a token if it is expired, but the application seems to get caught in an endless call of 401 errors to the API when the token is successfully refreshed. When I step through the code, the token does indeed refresh if expired but then tries to refresh repeatedly.
I should also note that upon clicking the button again and calling the API again, the app picks up the new token and works properly afterward. Would love to get this working without so many console errors in the first place though.
Here is the interceptor (old)
import { Injectable, Injector } from "#angular/core";
import { Router } from "#angular/router";
import {
HttpClient,
HttpHandler, HttpEvent, HttpInterceptor,
HttpRequest, HttpResponse, HttpErrorResponse
} from "#angular/common/http";
import { AuthService } from "./auth.service";
import { Observable } from "rxjs/Observable";
#Injectable()
export class AuthResponseInterceptor implements HttpInterceptor {
currentRequest: HttpRequest<any>;
auth: AuthService;
constructor(
private injector: Injector,
private router: Router
) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.auth = this.injector.get(AuthService);
var token = (this.auth.isLoggedIn()) ? this.auth.getAuth()!.token : null;
if (token) {
// save current request
this.currentRequest = request;
return next.handle(request)
.do((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
// do nothing
}
})
.catch(error => {
return this.handleError(error)
});
}
else {
return next.handle(request);
}
}
handleError(err: any) {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
// JWT token might be expired:
// try to get a new one using refresh token
console.log("Token expired. Attempting refresh...");
this.auth.refreshToken()
.subscribe(res => {
if (res) {
// refresh token successful
console.log("refresh token successful");
// re-submit the failed request
var http = this.injector.get(HttpClient);
http.request(this.currentRequest).subscribe(
(result: any) => {
console.log(this.currentRequest);
}, (error: any) => console.error(error)
);
}
else {
// refresh token failed
console.log("refresh token failed");
// erase current token
this.auth.logout();
// redirect to login page
this.router.navigate(["login"]);
}
}, error => console.log(error));
}
}
return Observable.throw(err);
}
}
EDIT:
Updated code to working solution
import { Injectable, Injector } from "#angular/core";
import { Router } from "#angular/router";
import {
HttpClient,
HttpHandler, HttpEvent, HttpInterceptor,
HttpRequest, HttpResponse, HttpErrorResponse, HttpHeaders
} from "#angular/common/http";
import { AuthService } from "./auth.service";
import { Observable, Subject } from "rxjs";
#Injectable()
export class AuthResponseInterceptor implements HttpInterceptor {
auth: AuthService;
currentRequest: HttpRequest<any>;
constructor(
private injector: Injector,
private router: Router
) { }
logout() {
this.auth.logout();
this.router.navigate(["login"]);
}
intercept(
request: HttpRequest<any>,
next: HttpHandler): Observable<HttpEvent<any>> {
this.auth = this.injector.get(AuthService);
let token = (this.auth.isLoggedIn()) ? this.auth.getAuth()!.token : null;
this.currentRequest = request;
return next.handle(request).
catch((error) => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.auth.refreshToken()
.switchMap(() => {
let token = (Response) ? this.auth.getAuth() : null;
console.log(token);
if (token) {
this.currentRequest = request.clone({
setHeaders: {
Authorization: `Bearer ${token.token}`
}
});
}
return next.handle(this.currentRequest);
}).
catch((e) => {
this.logout();
console.error(e);
return Observable.empty();
});
}
return Observable.throw(error);
});
}
}
Auth.service
constructor(private http: HttpClient,
#Inject(PLATFORM_ID) private platformId: any) {
}
// performs the login
login(username: string, password: string): Observable<boolean> {
var url = "api/token/auth";
var data = {
username: username,
password: password,
client_id: this.clientId,
// required when signing up with username/password
grant_type: "password",
// space-separated list of scopes for which the token is issued
scope: "offline_access profile email"
};
return this.getAuthFromServer(url, data);
}
// try to refresh token
refreshToken(): Observable<boolean> {
var url = "api/token/auth";
var data = {
client_id: this.clientId,
// required when signing up with username/password
grant_type: "refresh_token",
refresh_token: this.getAuth()!.refresh_token,
// space-separated list of scopes for which the token is issued
scope: "offline_access profile email"
};
return this.getAuthFromServer(url, data);
}
// retrieve the access & refresh tokens from the server
getAuthFromServer(url: string, data: any): Observable<boolean> {
return this.http.post<TokenResponse>(url, data)
.map((res) => {
let token = res && res.token;
// if the token is there, login has been successful
if (token) {
// store username and jwt token
this.setAuth(res);
// successful login
return true;
}
// failed login
return Observable.throw('Unauthorized');
})
.catch(error => {
return new Observable<any>(error);
});
}
// performs the logout
logout(): boolean {
this.setAuth(null);
return true;
}
// Persist auth into localStorage or removes it if a NULL argument is given
setAuth(auth: TokenResponse | null): boolean {
if (isPlatformBrowser(this.platformId)) {
if (auth) {
localStorage.setItem(
this.authKey,
JSON.stringify(auth));
}
else {
localStorage.removeItem(this.authKey);
}
}
return true;
}
// Retrieves the auth JSON object (or NULL if none)
getAuth(): TokenResponse | null {
if (isPlatformBrowser(this.platformId)) {
var i = localStorage.getItem(this.authKey);
if (i) {
return JSON.parse(i);
}
}
return null;
}
// Returns TRUE if the user is logged in, FALSE otherwise.
isLoggedIn(): boolean {
if (isPlatformBrowser(this.platformId)) {
return localStorage.getItem(this.authKey) != null;
}
return false;
}
return this.auth.refreshToken(response:any)
//response can be true or null
let token=(response)?this.auth.getAuth():null;
//In token we have an object of type TokenResponse
console.log(token)
.switchMap(() => {
if (token) {
this.currentRequest = request.clone({
setHeaders: { //I think it's toke.token
Authorization: `Bearer ${token.token}`
}
});
....
NOTE: Try to change "var" for "let"
NOTE2: At first you have
var token = (this.auth.isLoggedIn()) ? this.auth.getAuth()!.token : null;
// May be remove "!"?
let token = (this.auth.isLoggedIn()) ? this.auth.getAuth().token : null;
If you want separate the error handle you can do some like
handleError(err: any) {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
this.auth.refreshToken()
.switchMap(res=>{ //<--switchMap, not susbcribe
if (res) {
console.log("refresh token successful");
// re-submit the failed request
var http = this.injector.get(HttpClient);
//Modify there this.currentRequest if was neccesary
return next.handle(this.currentRequest).catch(error:any=>
{
console.error(error);
return Observable.throw(error);
});
}
else {
console.log("refresh token failed");
this.auth.logout();
this.router.navigate(["login"]);
}
})
}
}
return Observable.throw(err);
}