The feature I am working on is that the system should determine a correct type of user and allow appropriate permissions after successful login and what I had in mind is to use RoleGuard feature of nest JS.
But I can't seem to figure out how this RoleGuard really works.And can't seem to make it work.
I wanted to allow user with specific rules to access some endpoints , like only user with admin role is allowed to get all list of users.
What seem to be the issue ? Anyone has an idea ? I have provided snippets below. And upon requesting should I'll just be adding role in the request body ? or it would be good if Ill get the current logged user and determine the role ? Thank you.
Here is my user data which has the role:
"id": 11, {
"role": "admin",
"username": "myadmin#test.com",
"created": "2020-03-18T02:30:04.000Z",
"updated": "2020-03-18T03:02:12.000Z"
}
SampleCode
import { JwtAuthGuard } from '../../auth/jwt-auth.guard';
import { RolesGuard } from '../../common/guards/roles.guard';
import { Roles } from '../common/decorators/roles.decorator';
#Controller('user')
#UseGuards(JwtAuthGuard, RolesGuard)
export class UserController {
constructor(private readonly usersService: UsersService) {}
#Get()
#Roles('admin')
findAll(): Promise<UserEntity[]> {
return this.usersService.findAll();
}
RoleGuard
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
console.log('roles:', roles);
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
const hasRole = () => user.roles.some(role => roles.indexOf(role) > -1);
console.log('hasRole', hasRole);
return user && user.roles && hasRole();
}
}
To this code works you need to add User obj into request context using an AuthGuard.
First off you don`t need a JwtAuthGuard if you not implement another things the Standard AuthGuard do, Adding JwtAuthGuard into UseGuards decorator mades a overwrite of default AuthGuard and if you not adding the user obj into request obj inside of JwtAuthGuard code, the RolesGuard not will work correctly.
Standard Approach
If you see the source code
into the line 48 the attaches the user obj into request obj. After seeing this it`s simply.. just add into #UseGuards decorator the AuthGuard('jwt'),RolesGuards like that
import { AuthGuard } from '#nestjs/passport';
import { RolesGuard } from '../../common/guards/roles.guard';
import { Roles } from '../common/decorators/roles.decorator';
#Controller('user')
#UseGuards(AuthGuard('jwt'), RolesGuard)
export class UserController {
constructor(private readonly usersService: UsersService) {}
#Get()
#Roles('admin')
findAll(): Promise<UserEntity[]> {
return this.usersService.findAll();
}
Doing that the Standard Approach to RolesGuards wil runs correctly...
Now if you doing a different things and need a custom AuthGuard.. You need to add the User Obj returned of the validate function of JwtStrategy Class. Like that:
import { Injectable, ExecutionContext } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err: any, user: any, info: any, context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
request.user = user;
return user;
}
}
Related
I am fairly new to angular. I have two components namely header and profile component. The header component handles the login functionality and maintains two information- the user details which is json object and a isLoggedIn which is a boolean that saves current state of login. The general layout of the profile page is-
<header-component>
<profile-component>
Now since the header component handles the login. I want to avoid writing the logic for getting userDetails and the isLoggedIn status again for profile component. So i decided writing a shared service called profile service so that i can upload userDetails and isLogged from header and access that info in the profile component. The input in the loginlogout method comes from the header component.
SharedService code -
import { Injectable } from '#angular/core';
import { HttpService } from './https.service';
import { Observable, BehaviorSubject, of as observableOf } from 'rxjs';
import * as _ from 'lodash';
import { HttpHeaders, HttpParams } from '#angular/common/http';
import { BaseService } from './base.service';
#Injectable()
export class ProfileServices{
constructor(){};
userDetailsBS = new BehaviorSubject<any>('original value');
userDetails= this.userDetailsBS.asObservable();
isLoggedIn:boolean;
loginlogout(userDetails:any , isLoggedIn:boolean){
this.userDetails=userDetails;
this.userDetailsBS.next(this.userDetails);
console.log("Value of user details set in profile service",this.userDetails); //debug
console.log(".getValue() method:",this.userDetailsBS.getValue()); //debug
this.isLoggedIn=isLoggedIn;
}
getUserDetails(){
return this.userDetailsBS.getValue();
}
}
Post login from the header-component.ts i call the loginlogout method in the profile service to set the values. I also tried to access the value passed to the shared Service using the getUserDetails which shows that the userDetails object is passed correctly to the shared service.
The issue arises when i try to access the data from the profile component-
export class ProfileT1Component implements OnInit {
userDetails:any;
constructor(
public profileService: ProfileServices){
this.profileService.userDetails.subscribe((result)=>{
console.log(result);
this.userDetails=result;
console.log("received user details in profile component constructor: ", this.userDetails);
})
}
}
the result still shows "original value" and not the updated value. Is this wrong approach altogether or am i handling the observables incorrectly. Help would be much appreciated.
You need to make a couple of changes in your service to make it work. Add providedIn: root and remove all declarations from other modules. Secondly, you do not need this.userDetailsBS.asObservable() and you can use the subscribe directly on userDetailsBS. Your code will look something like the following.
Service:
#Injectable({
providedIn: 'root'
})
export class ProfileServices {
constructor() {}
userDetailsBS = new BehaviorSubject<any>('original value');
isLoggedIn: boolean;
loginlogout(userDetails: any, isLoggedIn: boolean) {
this.userDetailsBS.next(userDetails);
this.isLoggedIn = isLoggedIn;
}
getUserDetails() {
return this.userDetailsBS.getValue();
}
}
Component:
export class ProfileT1Component implements OnInit {
userDetails: any;
constructor(public profileService: ProfileServices) {
this.profileService.userDetailsBS.subscribe((result) => {
console.log(result);
this.userDetails = result;
console.log('received user details in profile component constructor: ', this.userDetails);
});
}
}
the implementation seems to be OK
(except you should make the BehaviorSubject private and expose only the observable)
probably you have multiple instance of the service.
try to add :
#Injectable({
providedIn: 'root',
})
and remove the service declaration from all the modules provider array
https://angular.io/guide/singleton-services
I am trying to get started with NestJS which uses TypeORM.
I have connected to a database. At least I think I have, because I was getting a lot of errors and after enough tuning of the config, the errors went away and the connection seems to be successful.
So now I want to fetch any bit of data just to get started.
There is a table in the database called RESULT_PAGE, so I just want to fetch any record from that. This is what I have tried:
result-page.entity.ts
import { Entity, PrimaryColumn, Column } from "typeorm";
#Entity()
export class ResultPage {
#PrimaryColumn()
result_page_id: number;
#Column({ length: 1 })
approval: string;
#Column({ length: 1})
manually_uploaded: string;
}
result-page.controller.ts
import { Controller, Get, Request } from '#nestjs/common';
import { ResultPageService } from './result-page.service';
#Controller('result-page')
export class ResultPageController {
constructor(
private resultPageService: ResultPageService
) { }
#Get('get-all')
getProfile() {
return this.resultPageService.findAll();
}
}
result-page.service.ts
import { Injectable } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { Repository } from 'typeorm';
import { ResultPage } from './result-page.entity';
#Injectable()
export class ResultPageService {
constructor(
#InjectRepository(ResultPage)
private readonly resultPageRepository: Repository<ResultPage>,
) {}
findAll(): Promise<ResultPage[]> {
return this.resultPageRepository.find();
}
}
If I edit the service to look like this:
import { Injectable } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { Repository } from 'typeorm';
import { ResultPage } from './result-page.entity';
#Injectable()
export class ResultPageService {
constructor(
#InjectRepository(ResultPage)
private readonly resultPageRepository: Repository<ResultPage>,
) {}
findAll(): Promise<string> {
return new Promise((resolve, reject) => { resolve('hello world') })
// return this.resultPageRepository.find();
}
}
then I get 'hello world', so it is definitely that the RESULT_PAGE table isn't connected
In the AppModule I am loading the entities like this
const typeOrmModuleOptions: TypeOrmModuleOptions = {
...
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true
}
I am sure that I am making some absolute noob mistake so if someone could help me out here it would be greatly appreciated. I am quite new to databases and api's so any info would help a lot. TIA
SOLVED
Solved by adding a connectString https://github.com/typeorm/typeorm/issues/3484#issuecomment-472315370
A little bit confusing the question. I miss the error and the database config.
result_page vs RESULT_PAGE: tablename
on linux/unix the tablenames are case sensitive, so you should set it in the annotation
#Entity({name: 'RESULT_PAGE'})
Please give some details to find the root cause if it was not that.
I'm trying to figure out how to limit a collection to returning just the user's data, not everyone's data.
In the example I'm working from the FirebaseService only shows CRUD examples where the data that's returned is everything.
import { Injectable } from "#angular/core";
import { Platform } from 'ionic-angular';
import 'rxjs/add/operator/toPromise';
import { AngularFirestore } from 'angularfire2/firestore';
import * as firebase from 'firebase/app';
import 'firebase/storage';
#Injectable()
export class FirebaseService {
constructor(
public afs: AngularFirestore,
public platform: Platform
){}
getEvents(){
return new Promise<any>((resolve, reject) => {
this.afs.collection('/events').snapshotChanges() // add +auth.uid ... or something?
.subscribe(snapshots => {
resolve(snapshots)
})
})
}
...
In order to only get the user's events back, I think I need to add:
import { AngularFireAuth } from 'angularfire2/auth';
... and, do something from there. But, I'm at a loss. Any help would be greatly appreciated. Thanks in advance!
You can limit this by adding rules. For example, you are using /users/ node to store user information. You can restrict only for the logged in user matching with userId can access /users/
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function isOwner(userId) {
return request.auth.uid == userId
}
match /users/{userId} {
allow get: if isSignedIn()
&& isOwner(userId);
....
}
}
To get User Id
constructor(
private afAuth: AngularFireAuth )
// then
ngOnInit() {
this.afAuth.authState;
this.afAuth.authState.subscribe(
user => {
this.userInfo = user; <-- You can store user Id information to user variable
},
err => {
console.log(err);
}
}
you can use this.userInfo.uid to make further calls.
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'm creating an Angular 2 SPA for learning purposes and integrating Auth0 for handeling the authentication. I have an auth.service.ts that is going to be called from difference places in my application, for example in the top-navbar to logout and on the auth-page to handle logins and registrations.
When trying to place the Auth0 container in a div by setting the container option I get the following error: Can't find element with id auth-container
How can I let the auth.service know how/where to look for the auth-container div? Placing all the logic inside the auth.component.ts is assumably not an option because the auth.service will be used for other functionality in other places where the lock variable is also used.
auth.service.ts
import { Injectable } from '#angular/core';
import { tokenNotExpired } from 'angular2-jwt';
import { myConfig } from './auth.config';
declare var Auth0Lock: any;
var options = { container: 'auth-container' };
#Injectable()
export class Auth {
lock = new Auth0Lock(myConfig.clientID, myConfig.domain, options);
constructor() {
this.lock.on('authenticated', (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
});
}
public authenticated() {
return tokenNotExpired();
};
public logout() {
localStorage.removeItem('id_token');
};
}
auth.component.ts
constructor(public auth: Auth) {
auth.lock.show();
}
auth.component.html
<div id="auth-container"></div>
Well they did not make your life easy but by mistake I made it work.
Try this:
auth.component.ts
ngOnInit() {
this.auth.login()
}
Delete this from your constructor
auth.lock.show();
The auth.service is not a container, it's a service that provides a popup when the login function is invoked.
So, to reuse it wherever you like, you need to inject the auth service into the component where you want to call the auth service from. Then, you just call the method. For example, here is the html for my Start component. You can see that the click event for the signin button is bound to the "submitLogin()" method of the component (the Start component):
<div class="splash-back" *ngIf="!authService.authenticated()">
<div id="splash">
<div id="logo"><span class="silver">GCO</span>TeamKeeper
<p class="silver tagline">The other teams could make trouble for us if they win.</p>
<p class="silver attribution">~ Yogi Berra</p></div>
<div class="call">
<br>
<button class="btn-sign-in" (click) = "submitLogin()">Sign up or Log in</button>
</div>
<!--<mtm-authentication></mtm-authentication>-->
</div>
</div>
And here is the start component code (note the injection of the authentication service in the constructor):
#Component({
selector: 'tk-start',
templateUrl: './start.component.html',
styleUrls: ['./start.component.css']
})
export class StartComponent implements OnInit {
constructor(private authService: UserAuthenticationService) { }
ngOnInit() {
}
submitLogin(){
this.authService.login();
}
}
And to make this example complete, here is my auth service code:
import {Injectable} from "#angular/core";
import { tkConfig } from './user-authentication.config';
import {Router} from "#angular/router";
import {tokenNotExpired} from "angular2-jwt";
let Auth0Lock = require('auth0-lock').default;
#Injectable()
export class UserAuthenticationService {
// Configure Auth0
userProfile: Object;
lock = new Auth0Lock (tkConfig.clientID, tkConfig.domain, {
avatar: null,
theme: {
primaryColor: "#69BE28",
foregroundColor: "#000000"
},
languageDictionary: {
title: "GCO TeamKeeper"
}
}
);
constructor(
private router: Router) {
this.userProfile = JSON.parse(localStorage.getItem('profile'));
// Add callback for lock `authenticated` event
this.lock.on('authenticated', (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
alert(error);
return;
}
profile.user_metadata = profile.user_metadata || {};
localStorage.setItem('profile', JSON.stringify(profile));
this.userProfile = profile;
this.router.navigate(['/organization']);
});
})
}
public login() {
// Call the show method to display the widget.
this.lock.show();
};
public authenticated() {
// Check if there's an unexpired JWT
// It searches for an item in localStorage with key == 'id_token'
return tokenNotExpired();
};
public logout() {
// Remove token from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
this.userProfile = undefined;
this.router.navigate(['/start']);
};
}