I am trying to check multiple condition to let user login. I was able implement authguard when user is logged In successfully. Not only user need validate but also need to meet the criteria to login.
Here is How I have implemented auth guard to check if user is Sign In or not.
export class AuthGuard implements CanActivate {
access: boolean;
constructor(
private auth: AuthService,
private router: Router,
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree {
return this.auth.user$.pipe(
take(1),
map((user) => (user ? true : false)),
tap((isLoggedIn) => {
if (!isLoggedIn) {
this.router.navigate(['/login']);
return false;
}
return true;
})
);
}
}
While registration user has a field called employee which is boolean. So I want to implement auth guard to login so that when user login it should meet the condition that the user credentials are valid and the user is employee
I tried Following by using following way but empolyee was undefined
this.auth.user$.subscribe((res) => {
this.isEmployee = res.isEmployee;
});
if (!this.isEmployee) {
console.log(this.isEmployee);
this.router.navigate(['/login']);
console.log('user is not employee');
return false;
}
console.log('user is approved', this.isEmployee);
return true;
}
Assuming you have keys isLoggedIn, isEmployee you can do following:
return this.auth.user$.pipe(
map((user) => {
if(user.isLoggedIn && user.isEmployee){
return this.router.navigate(['login']);
} else {
return false;
}
})
);
There is no point of using operator take(1) in a guard, user can attempt any number of times to get inside a page.
It seems that you are trying to implement authentication and authorization actions at the same time in your auth-guard.
You can implement user credentials and role check on your backend
authentication action and avoid complications in your guard.
Or you can visit this https://www.npmjs.com/package/keycloak-angular#authguard and check how both is implemented within single auth-guard.
Related
I'm working on a small personal app. I'll explain what I did until now and in the end my problem and my question.
I have created a Node server and an Angular app.
When the Angular app is booting I'm checking if the user is logged in (via http get request to the server, the request is made in app.component.ts)
ngOnInit(): void {
this.authService.checkIfUserSignedIn();
}
Inside the checkIfUserSignedIn method after that I'm getting the relevant authentication information I notify to the interested components with the auth state.
this.userAuthDetailsSubject.next(this.userAuthDetails);
Additionally, I'm having an AuthGuard that restrict the entry to the "create-list" component only to authenticated users.
In the AuthGurad I'm checking the auth state:
const authStatus = this.authService.isAuth();
return authStatus;
In the menu html component I have the following code:
<span routerLink="create-list" *ngIf="userIsAuthenticated"> New List</span>
Which works fine.
My problem is when i'm visiting manually localhost:4200/create-list
The AuthGuard is probably loaded before auth state is updated and therefore the user has no access to the "create-list" component, although he is signed in eventually.
I thought about two solutions but I'm not sure if they are good and how to implement them, and would like to hear your opinion.
using localStorage (It may be an overkill solution for this tiny problem)
make the HTTP get request to the server (for the auth state) inside the authGuard or maybe subscribe to an observer in the auth service (if so, how to implement that?)
Any ideas/solutions?
canActivate (AuthGuard):
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | import("#angular/router").UrlTree | import("rxjs").Observable<boolean | import("#angular/router").UrlTree> | Promise<boolean | import("#angular/router").UrlTree> {
const authStatus = this.authService.isAuth();
if (authStatus) {
return true;
} else {
this.router.navigate(['/login']);
}
}
auth.service.ts
#Injectable()
export class AuthService {
userAuthDetailsSubject = new Subject<UserAuthDetails>();
userAuthDetails: UserAuthDetails = null;
private isAuthenticated = false;
constructor(#Inject(DOCUMENT) private document: Document, private http: HttpClient) {
};
public isAuth(): boolean {
console.log({
isAuth: this.isAuthenticated
})
return this.isAuthenticated;
}
signIn() {
// redirect to signin..
this.document.location.href = '/auth/google';
}
signOut() {
this.document.location.href = '/auth/logout';
}
checkIfUserSignedIn() {
this.http.get<any>('/auth/current_user').subscribe(res => {
if (res) {
this.isAuthenticated = true;
console.log('assigning true to isAuth')
this.userAuthDetails = {
displayName: res.displayName,
email: res.email,
uid: res._id
};
this.userAuthDetailsSubject.next(this.userAuthDetails);
} else {
console.log('User not authenticated')
}
})
}
}
For this particular problem you can make the 'isAuthenticated' field a subject just like 'userAuthDetailsSubject' and update its value when the server responds.
auth.service.ts
checkIfUserSignedIn() {
this.http.get<any>('/auth/current_user').subscribe(res => {
if (res) {
this.isAuthenticated.next(true); //update the value
console.log('assigning true to isAuth')
this.userAuthDetails = {
displayName: res.displayName,
email: res.email,
uid: res._id
};
this.userAuthDetailsSubject.next(this.userAuthDetails);
} else {
console.log('User not authenticated')
}
})
}
Now change your authguard so it does not return true or false synchronously.
canActivate (AuthGuard):
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
boolean | import("#angular/router").UrlTree |
import("rxjs").Observable<boolean | import("#angular/router").UrlTree>| Promise<boolean | import("#angular/router").UrlTree> {
return this.authService.isAuth().subscribe((logged)=>{
if (logged) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
})
}
Off topic:
Why do you use import("#angular/router").UrlTree? You can use import like import { UrlTree } from '#angular/router';
CanActive support UrlTree return. return this.router.createUrlTree(['/login']); and not create a new async process in your canActivate
On Topic:
If you call direct link, you have to resolve authentication. If you call link or F5 reload browser will lost every data from memory. If you use any token to auth it be worth saving into localStore and restore from here.
Ofc, After authentication if you open new tab, this new tab will new without auth default like you used F5 on current tab. It lives a separate life for each tabs.
My auth is based on NGRX
so when the page starts loading I got all roles and
then get logged in.
but when I start using route guard,
route gourd start working before user data get loading
how can I wait for user load action to be done then start using canActivate
I try below solution but it's not working
export class AuthGuard implements CanActivate, OnDestroy {
private unsubscribe: Subject<any>;
constructor(private store: Store<AppState>, private router: Router,
private alertService: ToastrService, private authService: AuthService) {
this.unsubscribe = new Subject();
}
ngOnDestroy(): void {
this.unsubscribe.next();
this.unsubscribe.complete();
}
getFromStoreOrAPI(): Observable<any> {
return this.store.pipe(
select(isUserLoaded),
tap((data: boolean) => {
if (!data) {
this.store.dispatch(new UserRequested());
}
}),
take(1)
);
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.getFromStoreOrAPI().pipe(
switchMap(() =>
this.store.pipe(
select(isLoggedIn),
map(loggedIn => {
if (!loggedIn) {
this.router.navigateByUrl('/auth/login');
return false;
} else {
this.store.pipe(
select(currentUserRoles),
map((userRoles: Role[]) => {
//.......
}),
takeUntil(this.unsubscribe)
).subscribe();
}
}),
)
),
catchError(() => this.router.navigateByUrl('/auth/login'))
);
}
}
You can use filter to wait until loaded flag is true.
Here is approach I took with my auth.guard.ts :
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.authFacade.loaded$.pipe(
filter(loaded => !!loaded),
mergeMap(() => this.authFacade.userAccount$),
map(userAccount => {
if (!userAccount) this.authFacade.redirectLoginPage(state.url);
return !!userAccount;
}),
first()
);
}
In my case, main app component is dispatching an action CheckAuth to check if user is already authenticated, and then set loaded flag.
It should work with some adaptation for your need. But main difference is the use of filter which avoid to continue the workflow if user checking is not done, and force waiting for the value.
Of course, be sure to set loaded value in all the case after receiving response (authenticated or not), or in case of any error.
Here is a potential adaptation for your case :
authLoaded$ = this.store.pipe(select(authLoaded));
authAccount$ = this.store.pipe(select(authAccount));
canActivate(...) {
return userLoaded$.pipe(
tap(loaded => {
if (!loaded) {
this.store.dispatch(new UserRequested());
}
}),
filter(loaded => !!loaded),
mergeMap(() => authAccount$),
map(authAccount => {
if (!authAccount.loggedIn) {
this.router.navigateByUrl('/auth/login');
return false;
}
if (!authAccount.roles?.length) {
this.router.navigateByUrl('/auth/forbidden');
return false;
}
// do some extra stuff...
return true;
}),
first()
);
}
I renamed isUserLoaded to authLoaded to clearly indicate the status of authentication loading (you can use also ready or not for instance). But not necessary user.
I created also a new selector authAccount which returns an object with at least 2 things :
loggedIn : true/false if user is logged in
roles: array of user roles.
But you can add of course user property, with user details.
This is a composed selector from different parts of your state.
With it, your code is more clear and maintable, you receive a complete status of your current authentication user.
Maybe some typos is possible, I wrote the code directly in my answer without testing it.
Hope this will help you.
I'm going this direction because i've another guard who is validating the user on the token previously stored. This is what i was doing in previous version of rxjs, now in the latest you cant map on the subject.
canActivate(next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.authservice.behaviorsubject().map(x => {
if (x) {
return true;
} else {
return false;
}
})
}
I need any guidence how to get this thing working. I've tried somting like this but wont return anything since never subscribed to it (and also its not a Observable its a AnonymusSubject when i console.log it)
canActivate(next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.authservice.behaviorsubject().pipe(
map(x => {
if (x) {
return true;
} else {
return false;
}
})
)
}
Also tried converting it into observable with Observable.create() but wont work.
I know im missing something.
Heeeeelp :)
return your behaviorSubject as an observable in your authservice:
private _authed: BehaviorSubject<boolean> = new BehaviorSubject(null);
behaviorsubject():Observable<boolean> {
return this._authed.asObservable()
}
Also a small comment: CanActivate works with observables, so if your behaviorsubject function returns a Observable with type boolean, you don't need to map it
Current routing configuration:
//...
{
path: 'foo/:id',
component: SomeComponent,
canActivate: [SomeGuard]
},
//...
Then in guard I call permission service to get access for component:
#Injectable()
export class SomeGuard implements CanActivate {
constructor(private service: Service) {
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
const id = parseInt(next.params['id']);
return this.service.getPermissions(id).then(permissions => {
if (permissions.canView) {
return true;
}
return false;
});
}
}
But in the component I utilize the same permissions endpoint, which means I call it twice in a row to get to one page:
//...
constructor(private route: ActivatedRoute,
private service: Service) {
}
ngOnInit() {
this.id = parseInt(this.route.snapshot.params['id']);
this.service.getPermissions(this.id).then(permissions => {
// ...
});
}
//...
So it would be great to just save the permissions data in the route and utilize it by both guard and the component. I tried using resolve, but it turns out resolve only activates after the guards, which is no good. So how can i save permissions data?
This looks like the kind of task for a caching service. Permissions do not change often so they are the perfect candidate for caching. That way even multiple visits to the same resource would not trigger multiple HTTP requests for permission checks.
Edit: Since you need permissions to be loaded each time, you could listen for RouteNavigationStart and clear the cache. If this becomes too cumbersome to maintain in the PermissionsService you could extract the logic into a separate service.
You could something like this in the service you use to get your permissions:
// Permissions service
private permissionCache;
constructor(
router: Router,
) {
// clear cache when a route navigation starts
router.events
.filter(event => event instanceof NavigationStart)
.subscribe(event => this.permissionCache = {})
}
getPermissions(id) {
if (permissionCache[id]) {
return Observable.of(permissionCache[id]);
} else {
// get the permissions
permissionCache[id] = response;
return Observable.of(response);
}
});
I have an application where there are two guards (AuthGuard - for logged users, AdminGuard - for admins). The AuthGuard on the first loading makes http request to get the user information from the API. The Problem is when you try to access route with both guards, the AdminGuard does not wait for the AuthGuard to finish with the request and set the user so the AdminGuard can check the role of the user, and the application breaks. I know it breaks because the user is undefined.
I'm looking for a solution on how to make the second guard to wait for the first to finish.
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard, AdminGuard]
},
#Injectable()
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private http: HttpClient) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.http.get('https://jsonplaceholder.typicode.com/users').map(res => {
console.log('Auth Guard.');
console.log(res);
this.authService.user = {role: 'admin'};
return true;
});
return false;
}
}
#Injectable()
export class AdminGuard implements CanActivate {
constructor(private authService: AuthService) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
console.log('Admin Guard.');
console.log(this.authService.user);
if (this.authService.user.role === 'admin') {
return true;
}
return false;
}
}
Here is a plnker link - http://plnkr.co/edit/EqgruNjogTJvsC1Zt5EN?p=preview
Very important thing to understand is that in AuthGuard you make an asynchronous call and we don't know, when it will be resolved. Other code is synchronous and will be executed immediately without waiting this asynchronous call (it's why the user is undefined).
But you can force AdminGuard to wait, while your HTTP call will be resolved: to do that, you can store Observable Subscription (because you're working with observable, but you can do the same trick with promise too) to AuthService from AuthGuard (where you make your HTTP call) using the following row:
this.authService.subscription$ = this.http.get('https://jsonplaceholder.typicode.com/users');
Now your subscription is in AuthService, all that you need is to subscribe on it in both guards (you're using .map() in your case):
AuthGuard:
return this.authService.subscription$.map(res => {
this.authService.user = {role: 'admin'};
return true;
});
AdminGuard:
return this.authService.subscription$.map(res => {
if (this.authService.user.role === 'admin') {
return true;
}
});
Here is the working plunker:
http://plnkr.co/edit/R2Z26GsSvzEpPdU7tOHO?p=preview
If you see "AuthGuard returns TRUE!" and "AdminGuard returns TRUE!" in your console - everything should work fine. I have also logged this.authService.user variable from both AuthGuard and AdminGuard.