I'm using Angular 4 with ADAL to authenticate users in my web application, using ng2-adal library which is a wrapper for adal.js.
The problem I'm facing is the following:
So the token expires after a time limit and I have a canActivate route guard that checks if the user is authenticated. If not, it navigates the users to the login page. This is how my route guard is looking:
import { Injectable } from '#angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '#angular/router';
import { AdalService } from 'ng2-adal/dist/core';
#Injectable()
export class RouteGuard implements CanActivate {
constructor(private router: Router, private adalService: AdalService) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (this.adalService.userInfo.isAuthenticated) {
return true;
} else {
this.router.navigate(['/user-login']);
return false;
}
}
}
so whenever the token expires, the user is navigated to the login page, which is annoying for the users. Is there a way to renew the token whenever it expires?
I figured it out. This is how I added it:
import { Injectable } from '#angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '#angular/router';
import { AdalService } from 'ng2-adal/dist/core';
#Injectable()
export class RouteGuard implements CanActivate {
constructor(private router: Router, private adalService: AdalService) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (this.adalService.userInfo.isAuthenticated) {
return true;
} else {
this.adalService.acquireToken(this.adalService.config.clientId).toPromise().then((data) => {
console.log('Generating a new authentication token.');
return true;
},
(error) => {
console.log('No user logged in.');
this.router.navigate(['/user-login']);
return false;
}
}
}
}
I had the same issue and my fix worked.
In app.component.ts, add this code to ngOnit().
this.adalService.handleWindowCallback();
this.adalService.acquireToken(this.adalService.config.loginResource).subscribe(token => {
this.adalService.userInfo.token = token;
if (this.adalService.userInfo.authenticated === false) {
this.adalService.userInfo.authenticated = true;
this.adalService.userInfo.error = '';
}
}, error => {
this.adalService.userInfo.authenticated = false;
this.adalService.userInfo.error = error;
this.adalService.login();
});
When token expires, app component gets called, and acquire token refreshes the token silently. But the this.adalService.userInfo.authenticated is still false leading to redirection or again calling login method. So manually setting it to true fixes the redirection error. this.adalService.config.loginResource this is automactically set by adal-angular itself with the resource that we need token for.
Also add expireOffsetSeconds: 320, to adal configuration data settings along with
tenant: configData.adalConfig.tenant,
clientId: configData.adalConfig.clientId,
redirectUri: window.location.origin,
expireoffsetseconds invalidates the token based on the time that we specify before its actual expiry.
Related
I have my application up and running with Angular 2.1.0.
The routes are protected via router Guards, canActivate.
When pointing the browser to a protected area like "localhost:8080/customers" I get redirected to my login page just like expected.
But after a successful login, I would like to be redirected back to calling URL ("/customers" in this case).
The code for handling the login looks like this
login(event, username, password) {
event.preventDefault();
var success = this.loginService.login(username, password);
if (success) {
console.log(this.router);
this.router.navigate(['']);
} else {
console.log("Login failed, display error to user");
}
}
The problem is, I don't know how to get a hold of the calling url from inside the login method.
I did find a question (and answer) regarding this but couldn't really make any sense of it.
Angular2 Redirect After Login
There's a tutorial in the Angular Docs, Milestone 5: Route guards. One possible way to achieve this is by using your AuthGuard to check for your login status and store the url on your AuthService.
AuthGuard
import { Injectable } from '#angular/core';
import {
CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot
} from '#angular/router';
import { AuthService } from './auth.service';
#Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
let url: string = state.url;
return this.checkLogin(url);
}
checkLogin(url: string): boolean {
if (this.authService.isLoggedIn) { return true; }
// Store the attempted URL for redirecting
this.authService.redirectUrl = url;
// Navigate to the login page with extras
this.router.navigate(['/login']);
return false;
}
}
AuthService or your LoginService
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Router } from '#angular/router';
#Injectable()
export class AuthService {
isLoggedIn: boolean = false;
// store the URL so we can redirect after logging in
public redirectUrl: string;
constructor (
private http: Http,
private router: Router
) {}
login(username, password): Observable<boolean> {
const body = {
username,
password
};
return this.http.post('api/login', JSON.stringify(body)).map((res: Response) => {
// do whatever with your response
this.isLoggedIn = true;
if (this.redirectUrl) {
this.router.navigate([this.redirectUrl]);
this.redirectUrl = null;
}
}
}
logout(): void {
this.isLoggedIn = false;
}
}
I think this will give an idea how things work, of course you probably need to adapt to your code
This code will handle your request:
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService,
private router: Router) {
}
canActivate(next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> {
return this.authService.isVerified
.take(1)
.map((isVerified: boolean) => {
if (!isVerified) {
this.router.navigate(['/login'], {queryParams: {returnUrl: state.url}});
return false;
// return true;
}
return true;
});
}
}
but be aware that the URL params will not pass with the URL!!
You can find a nice tutorial here :
http://jasonwatmore.com/post/2016/12/08/angular-2-redirect-to-previous-url-after-login-with-auth-guard
The answers I saw were correct.
But the best way to answer your question is returnUrl.
like this:
export class AuthGuardService implements CanActivate {
constructor(private auth: AuthenticationService, private router: Router) { }
canActivate(next: ActivatedRouteSnapshot,
_state: import('#angular/router').RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
let isLoggedIn = false;
const idToken = next && next.queryParamMap.get('id_token');
try {
const expiresAt = idToken && JSON.parse(window.atob(idToken.split('.')[1])).exp * 1000;
if (idToken && expiresAt) {
isLoggedIn = true;
localStorage.setItem('id_token', idToken);
localStorage.setItem('expires_at', String(expiresAt));
} else {
isLoggedIn = this.auth.isLoggedIn();
}
} catch (e) {
console.error(e);
isLoggedIn = this.auth.isLoggedIn();
}
if (!isLoggedIn) {
//this section is important for you:
this.router.navigate(['/login'], { queryParams: { returnUrl: _state.url }});
}
return isLoggedIn;
}
}
This navigate create a url with returnUrl like a param, now you can read returnUrl from param.
GoodLuck.
I am using auth0 to configure my angular 7 app with auth. I followed the quickstart guide to get up and running. The only thing I added in addition to the auth.service.ts file is:
getTokenSilently$(options?): Observable<string> {
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getTokenSilently(options)))
);
}
to support the HTTP interceptor:
import { Injectable } from '#angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '#angular/common/http';
import { AuthService } from './auth.service';
import { Observable, throwError } from 'rxjs';
import { mergeMap, catchError } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class InterceptorService implements HttpInterceptor {
constructor(private auth: AuthService) { }
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return this.auth.getTokenSilently$().pipe(
mergeMap(token => {
const tokenReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next.handle(tokenReq);
}),
catchError(err => throwError(err))
)
}
}
The other thing I did differently is that my app does not have a login button, when the app starts up it trys to go to the home page which has an auth guard:
import { Injectable } from '#angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, CanActivate } from '#angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
import { tap } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean|UrlTree> | boolean {
return this.auth.isAuthenticated$.pipe(
tap(loggedIn => {
if (!loggedIn) {
this.auth.login(state.url);
}
})
);
}
}
so it automatically calls the login so the user gets redirected to auth0. Once they enter their creds it then redirects them back to the home page. Then all of a sudden the auth.service.ts service is loaded again and the auth guard has fired again and it fires the login function again. This seems to just keep on looping.
Any idea why it just keeps on looping?
Replace your Guard method by this one :
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean | UrlTree> | boolean {
return this.auth.isAuthenticated$.pipe(
delay(1000),
tap(loggedIn => {
if (!loggedIn) {
this.auth.login(state.url);
}
})
);
}
The problem was that loggedIn is still at false, even after a successful login.
I am aware that this is not a clean or pretty solution.
I modestly think that the Auth0 Angular sample needs some fixes.
Note: This answer could be helpful to avoid the redirect loop issue for anyone building a Angular SPA
And using the auth0 angular SDK's inbuilt AuthGuard
Or/And using the auth0 provided login page with callback URL
Or facing auth.isAuthenticated$ is always false issue
Try setting the 'Application Type' to 'Single Page Application' in auth0 application properties page
The documentation is kinda thin here so I ran into a problem. I try to use Guards to secure Controller or it's Actions, so I gonna ask for the role of authenticated requests (by JWT). In my auth.guard.ts I ask for "request.user" but it's empty, so I can't check the users role. I don't know how to define "request.user". Here is my auth module and it's imports.
auth.controller.ts
import { Controller, Get, UseGuards } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
import { AuthService } from './auth.service';
import { RolesGuard } from './auth.guard';
#Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
#Get('token')
async createToken(): Promise<any> {
return await this.authService.signIn();
}
#Get('data')
#UseGuards(RolesGuard)
findAll() {
return { message: 'authed!' };
}
}
roles.guard.ts
Here user.request is empty, because I never define it. The documentation doesn't show how or where.
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user; // it's undefined
const hasRole = () =>
user.roles.some(role => !!roles.find(item => item === role));
return user && user.roles && hasRole();
}
}
auth.module.ts
import { Module } from '#nestjs/common';
import { AuthService } from './auth.service';
import { HttpStrategy } from './http.strategy';
import { UserModule } from './../user/user.module';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '#nestjs/passport';
import { JwtModule } from '#nestjs/jwt';
#Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secretOrPrivateKey: 'secretKey',
signOptions: {
expiresIn: 3600,
},
}),
UserModule,
],
providers: [AuthService, HttpStrategy],
controllers: [AuthController],
})
export class AuthModule {}
auth.service.ts
import { Injectable } from '#nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '#nestjs/jwt';
#Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}
async signIn(): Promise<object> {
// In the real-world app you shouldn't expose this method publicly
// instead, return a token once you verify user credentials
const user: any = { email: 'user#email.com' };
const token: string = this.jwtService.sign(user);
return { token };
}
async validateUser(payload: any): Promise<any> {
// Validate if token passed along with HTTP request
// is associated with any registered account in the database
return await this.userService.findOneByEmail(payload.email);
}
}
jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from './auth.service';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable, UnauthorizedException } from '#nestjs/common';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'secretKey',
});
}
async validate(payload: any) {
const user = await this.authService.validateUser(payload);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Documentation: https://docs.nestjs.com/guards
Thanks for any help.
Additionally to your RolesGuard you need to use an AuthGuard.
Standard
You can use the standard AuthGuard implementation which attaches the user object to the request. It throws a 401 error, when the user is unauthenticated.
#UseGuards(AuthGuard('jwt'))
Extension
If you need to write your own guard because you need different behavior, extend the original AuthGuard and override the methods you need to change (handleRequest in the example):
#Injectable()
export class MyAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info: Error) {
// don't throw 401 error when unauthenticated
return user;
}
}
Why do this?
If you look at the source code of the AuthGuard you can see that it attaches the user to the request as a callback to the passport method. If you don't want to use/extend the AuthGuard, you will have to implement/copy the relevant parts.
const user = await passportFn(
type || this.options.defaultStrategy,
options,
// This is the callback passed to passport. handleRequest returns the user.
(err, info, user) => this.handleRequest(err, info, user)
);
// Then the user object is attached to the request
// under the default property 'user' which you can change by configuration.
request[options.property || defaultOptions.property] = user;
You can attach multiple guards together (#UseGuards(AuthGuard('jwt'), RolesGuard)) to pass the context between them. Then you will have access 'req.user' object inside 'RolesGuard'.
After I got the selected answer working (thank you), I found this option as well that you can add to the constructor that essentially does the same thing.
http://www.passportjs.org/docs/authorize/
Association in Verify Callback
One downside to the approach described above is that it requires two
instances of the same strategy and supporting routes.
To avoid this, set the strategy's passReqToCallback option to true.
With this option enabled, req will be passed as the first argument to
the verify callback.
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
constructor(private authService: AuthService) {
super({
passReqToCallback: true
})
}
// rest of the strategy (validate)
}
Does it work if you use req.authInfo?
As long as you don't provide a custom callback to passport.authenticate method, the user data should be attached to the request object like this.
req.authInfo should be the object you returned in your validate method
I guess it's quite simple issue, but unfortunately I don't really know how to deal with it.
I'm trying to connect my UserAuthenticationService service with the ActivationGuard.
UserAuthenticationService.ts:
import {Injectable} from '#angular/core';
import {Http} from '#angular/http';
#Injectable()
export class UserAuthenticationService {
isUserAuthenticated: boolean = false;
username: string;
constructor(private http: Http) {
}
authentication() {
this.http.get(`http://localhost/api/auth/isLogged/${this.username}`)
.subscribe(res => { //^^returns true or false, depending if the user is logged or not
this.isUserAuthenticated = res.json();
},
err => {
console.error('An error occured.' + err);
});
}
}
ActivationGuard.ts
import {Injectable} from '#angular/core';
import {Router, RouterStateSnapshot, ActivatedRouteSnapshot} from '#angular/router';
import {Observable} from 'rxjs/Observable';
import {UserAuthenticationService} from './UserAuthenticationService';
interface CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean>|Promise<boolean>|boolean
}
#Injectable()
export class WorksheetAccessGuard implements CanActivate {
constructor(private router: Router, private userService: UserAuthenticationService) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (this.userService) {
this.router.navigate(['/']);
return false;
}
return true;
}
}
Note
It works great, if I just use localStorage to store the information if the user is logged or not:
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (!localStorage.getItem('currentUser')) {
this.router.navigate(['/']);
return false;
}
return true;
}
But how can I connect the service with the guard? Looking forward for any kind of help. Thank you in advance.
If you need any more information, please let me know and I will edit my post.
Call authentication() method of UserAuthenticationService either in constructor or On ngOnit then it sets the isUserAuthenticated variable and use that in the ActivationGuard.ts
UserAuthenticationService.ts:
import {Injectable} from '#angular/core';
import {Http} from '#angular/http';
#Injectable()
export class UserAuthenticationService {
isUserAuthenticated: boolean = false;
username: string;
constructor(private http: Http) {
this.authentication();
}
authentication() {
this.http.get(`http://localhost/api/auth/isLogged/${this.username}`)
.subscribe(res => { //^^returns true or false, depending if the user is logged or not
this.isUserAuthenticated = res.json();
},
err => {
console.error('An error occured.' + err);
});
}
}
ActivationGuard.ts
#Injectable()
export class WorksheetAccessGuard implements CanActivate {
constructor(private router: Router, private userService: UserAuthenticationService) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (this.userService.isUserAuthenticated) {
this.router.navigate(['/']);
return false;
}
return true;
}
}
This is not the right approach for doing it. Every time you call the service , it initialize a new instance and hence you get a false.
You should create a singleton service instance ( via the main module in your app) - where it will contain your app state ( in memory / localstorage)
Then , when you'll call UserAuthenticationService - you won't update its owbn parameter but the main's one ( the singleton).
I suggest you to use a BehaviourSubject ( read about it , it's like a Subject but it also yields its last value without waiting to emit a value manually).
From that point your app can see from anywhere ig the user is logged in or not.
I'm using AngularFire2 for an app and I've gotten the registration/login functionality working with Firebase, however, every time I refresh the page, the logged in state is reset and won't persist. I can't quite find functionality to do this, though I feel I'm missing something very small.
Can I use the AngularFireAuth to check on page load somewhere?
Here is my auth provider code:
import { Injectable } from '#angular/core';
import {Observable, Subject, BehaviorSubject} from "rxjs/Rx";
import {AngularFireAuth, FirebaseAuthState} from "angularfire2";
import {AuthInfo} from "./auth-info";
import {Router} from "#angular/router";
#Injectable()
export class AuthService {
static UNKNOWN_USER = new AuthInfo(null);
authInfo$: BehaviorSubject<AuthInfo> = new BehaviorSubject<AuthInfo>(AuthService.UNKNOWN_USER);
constructor(private auth: AngularFireAuth, private router:Router) {
}
login(email, password):Observable<FirebaseAuthState> {
return this.fromFirebaseAuthPromise(this.auth.login({email, password}));
}
signUp(email, password) {
return this.fromFirebaseAuthPromise(this.auth.createUser({email, password}));
}
fromFirebaseAuthPromise(promise):Observable<any> {
const subject = new Subject<any>();
promise
.then(res => {
const authInfo = new AuthInfo(this.auth.getAuth().uid);
this.authInfo$.next(authInfo);
subject.next(res);
subject.complete();
},
err => {
this.authInfo$.error(err);
subject.error(err);
subject.complete();
});
return subject.asObservable();
}
logout() {
this.auth.logout();
this.authInfo$.next(AuthService.UNKNOWN_USER);
this.router.navigate(['/login']);
}
}
Thankyou in advance!
AngularFireAuth is an observable and emits FirebaseAuthState values. If a user is signed in and the page is refreshed, AngularFireAuth will emit an authenticated FirebaseAuthState; otherwise, it will emit null.
So something like this should come close to solving your problem:
constructor(private auth: AngularFireAuth, private router:Router) {
auth.subscribe((authState) => {
if (authState) {
const authInfo = new AuthInfo(authState.uid);
this.authInfo$.next(authInfo);
}
});
}