NestJS - how to check if JWT can access specific endpoint - node.js

Let's say I have 3 endpoints: A is for generating JWT token, B for accessing data of user X and C for accessing data of user Y. Now, what I want to do is, that I can from recieved token somehow in controller guards figure out, if user can access endpoint.
So, token generated for user X can only access endpoint B, token generated for user Y can only access endpoint C.
Token has to be generated at endpoint A, since users sign in at same form.
If question is unclear ask in comment.

You Can do that by specifying in the payload a role, by this role you set a guard on each endpoint which role has the access to it. let me give an example:
I believe that you have a function where you fill you payload kind of this function :
createJwtPayload(user){
let data: JwtPayload = {
userData: user,
companyId : user.company.id,
role:user.role.name, // for us this where we specify the role for our User
};
......
}
Now We have to create guards we need to specify access for x endpoints
let start with Admin Guard:
#Injectable()
export class AdminGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
if (!request.headers.authorization) {
return false;
}
request.user = await this.validateToken(request.headers.authorization);
if( request.user.role == ROLES.SUPER_ADMIN) {
return true;
}
return false;
}
async validateToken(auth: string) {
......
}
lets make the second guard we call it EmployeGuard :
....
#Injectable()
export class EmployeGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
if (!request.headers.authorization) {
return false;
}
request.user = await this.validateToken(request.headers.authorization);
if( request.user.role == ROLES.COMPANY_ADMIN || request.user.role == ROLES.USER) {
return true;
}
return false;
}
async validateToken(auth: string) {
......
}
Now to use these guards we just need to use #UseGuards() in our endpoint :
#Post()
#UseGuards(AdminGuard)
async addCompany(#Res() res, #Body() createDto: CompanyDto) {
........
}
#Get(':companyID')
#UseGuards(EmployeGuard)
async getcompany(#Res() res, #Param('companyID') companyID) {
....
}
Bonus: you can #useGuards on the controller to make sure the all endpoints use it

Related

How to access parent query arguments in GraphQL and Nestjs execution context

Let's say we have a bookshop and an author entity, to show the author their earnings stat, we want to check if the authenticated user is indeed the author themselves. So we have:
#UseGuards(GqlAuthGuard)
#ResolveField(() => [Eearning], { name: 'earnings' })
async getEarnings(
#Parent() author: Author,
#GqlUser() user: User,
) {
if (user.id !== author.id)
throw new UnauthorizedException(
'Each author can only view their own data',
);
// rest of the function implementation
}
We could query this:
query {
author(id: "2bd79-6d7f-76a332b06b") {
earnings {
sells
}
}
}
Now imagine we want to use a custom Guard instead of that if statement. Something like below:
#Injectable()
export class AutherGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const ctx = GqlExecutionContext.create(context);
// const artistId = ?
}
}
How can I access the id argument given to the author query when AutherGuard is used for the getEarnings handler?
Not sure how documented is that but the parent object can be accessed through the getRoot method:
const gqlContext = GqlExecutionContext.create(context);
const root = gqlContext.getRoot();
const authorId = root.id;
In fact, we have a helper function that we use like this:
export function getArgs(context: ExecutionContext): any {
if (context.getType<GqlContextType>() === "graphql") {
const gqlContext = GqlExecutionContext.create(context);
return { ...gqlContext.getArgs(), $parent: gqlContext.getRoot() };
} else if (context.getType() === "http") {
return context.switchToHttp().getRequest().params;
}
}
...
const args = getArgs(context);
const authorId = _.get(args, "$parent.id");

NestJS/GraphQL/Local Auth - How to define variables passed in

What is the standard approach to defining variables to be used only in guards/interceptors?
Resolver
Without the #Args() it throws an error when I send a mutation with variables
With the #Args(), it has an eslint error createUserInput' is defined but never used
#UseGuards(GqlLocalAuthGuard)
#Mutation(() => User)
async login(
// #Args("createUserInput") createUserInput: CreateUserInput,
// ^ it works if I put this
): Promise<User> {
return { id: "1", email: "fake#gmail.com" };
}
GqlLocalAuthGuard
This extends local auth guard and the strategy verifies that the user put in the correct password
export class GqlLocalAuthGuard extends AuthGuard("local") {
getRequest(context: ExecutionContext): any {
const ctx = GqlExecutionContext.create(context);
const req = ctx.getContext().req;
const { createUserInput } = ctx.getArgs<any>();
req.body = createUserInput;
return req;
}
}
Error without Args()
"GraphQLError: Unknown argument \"createUserInput\" on field \"Mutation.login\"."
Mutation
mutation ($createUserInput: CreateUserInput!) {
login (createUserInput: $createUserInput) {
id,
email,
}
}

Netstjs - multiple global guards with user access

I want to use the guard which will check the database if current user is active on each request. From the research I have done, I can use request.user only if the the guard is not global.
Options I have are:
Register guards on each controller/resolver (downside: duplicate code)
Extract and decode jwt from request.headers (jwt is then decoded twice: once from me and once under the hood (I am using AuthGuard('jwt')))
What is the best (and cleanest) solution for doing the needed filtering? Is there any better option available?
#Injectable()
export class IsUserActiveGuard implements CanActivate {
constructor(
private userService: UsersService) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;
const user = request.user;
if (user)
return await this.userService.checkActiveUser(user.id);
return true;
}
}
Move the checkActiveUser method to a new service, maybe some called GuardService and create a global module with GuardService as a provider, then import that global module to the root module (often called AppModule) and that's it. You can use your Guard on your application without importing the Module again.
Inside the guard you can have this code to get the token and payload
const req = context.switchToHttp().getRequest();
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) throw new UnauthorizedException('Null Access Token');
try {
verify(
token,
`${this.config.get<string>('JWT_ACCESS_KEY')}`,
) as IAccess;
req.session = user; //User Session
/* Some extra validation here */
} catch (error) {
throw new ForbiddenException('Invalid Access Token');
}
return true;

Nestjs Interceptor how to catch http 401 error and resubmit original request

I need to write an http header interceptor to add Authorization header, if there is a 401 error, submit another request for a new token, then resubmit the original request with the new token.
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
catchError(async error => {
if (error.response.status === 401) {
const originalRequest = error.config;
var authRes = await this.authenticationService.getAccessToken();
this.authenticationService.accessTokenSubject.next(authRes.access_token);
// I need to resubmit the original request with the new token from here
// but return next.handle(originalRequest) doesn't work
}
return throwError(error);
}),
);
}
But next.handle(originalRequest) doesn't work. How to resubmit the original request in the interceptor? Thank you very much in advance for your help.
I just encountered a similar problem, where I can catch the exception from exception filter but can't do so in interception layer.
So I looked up the manual and found it says:
Any exception thrown by a guard will be handled by the exceptions layer
(global exceptions filter and any exceptions filters that are applied to the current context).
So, if the exception is thrown from AuthGuard context(including the validate method in your AuthService), probably better to move the additional logic by extending the Authguard
like this:
export class CustomizedAuthGuard extends AuthGuard('strategy') {
handleRequest(err, user, info, context, status) {
if (err || !user) {
// your logic here
throw err || new UnauthorizedException();
}
return user;
}
}
or simply using customized exception filter.
It's been a while since the question but maybe it will help someone.
Ok, suppose that we need handle unauthorize exception out of route and guards, maybe service to service. So you can implement a interceptor like that and add some logic to get some data if needed, Ex: inject some Service in the interceptor.
So, throw an unauthorize exception and we are going to intercept it:
#Injectable()
export class UnauthorizedInterceptor implements NestInterceptor {
constructor(
private readonly authService: AuthService,
private readonly httpService: HttpService,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((err) => {
const {
response: { status, config },
} = err;
// assuming we have a request body
const jsonData = JSON.parse(config.data);
if (status === HttpStatus.UNAUTHORIZED) {
// We can use some data in payload to find user data
// here for example the user email
if (jsonData?.email) {
return
from(this.authService.getByUserEmail(jsonData.email)).pipe(
switchMap((user: User) => {
if (user) {
// Ex: we can have stored token info in user entity.
// call function to refresh access token and update user data
// with new tokens
return from(this.authService.refreshToken(user)).pipe(
switchMap((updatedUser: User) => {
// now updatedUser have the new accessToken
const { accessToken } = updatedUser;
// set the new token to config (original request)
config.headers['Authorization'] = `Bearer ${accessToken}`;
// and use the underlying Axios instance created by #nestjs/axios
// to resubmit the original request
return of(this.httpService.axiosRef(config));
}),
);
}
}),
);
} else {
return throwError(() => new HttpException(err, Number(err.code)));
}
} else {
return throwError(() => new HttpException(err, Number(err.code)));
}
}),
);
}
}

FeathersJS authentication deactivate user

I am using FeathersJS and been happy with authentication it provides. I this case it is local JWT. A client requested user management with an ability to disable some. There is field isDisabled in Users model, but it's hard to figure out where the check should be performed and how to set it up.
"#feathersjs/feathers": "^3.0.2",
"#feathersjs/authentication": "^2.1.0",
"#feathersjs/authentication-jwt": "^1.0.1",
"#feathersjs/authentication-local": "^1.0.2",
It depends where you want to check. You can either customize the JWT verifier or create a hook on the users service for the get method:
app.service('users').hooks({
after: {
get(context) {
const user = context.result;
if(user.isDisabled) {
throw new Error('This user has been disabled');
}
}
}
});
I did this directly in my authenticate hook:
const { authenticate } = require('#feathersjs/authentication').hooks
const { NotAuthenticated } = require('#feathersjs/errors')
const verifyIdentity = authenticate('jwt')
function hasToken(hook) {
if (hook.params.headers == undefined) return false
if (hook.data.accessToken == undefined) return false
return hook.params.headers.authorization || hook.data.accessToken
}
module.exports = async function authenticate(context) {
try {
await verifyIdentity(context)
} catch (error) {
if (error instanceof NotAuthenticated && !hasToken(context)) {
return context
}
}
if (context.params.user && context.params.user.disabled) {
throw new Error('This user has been disabled')
}
return context
}
You see I did check the just loaded user record and throw an error in case. And as this hook is called in before:all the user is rejected before any action is done.
As for feathers 4 you can extend your auth strategies very easily. For example if we want to user only be able to login and verify their JWT we would do the following in authentication.ts (Typescript):
import { Id, Query, ServiceAddons } from '#feathersjs/feathers';
import { AuthenticationService, JWTStrategy } from '#feathersjs/authentication';
import { LocalStrategy } from '#feathersjs/authentication-local';
import { expressOauth } from '#feathersjs/authentication-oauth';
import { Application } from './declarations';
declare module './declarations' {
interface ServiceTypes {
'authentication': AuthenticationService & ServiceAddons<any>;
}
}
Extend the local strategy by alter getEntityQuery to only inlcude users which are active.
class CustomLocalStrategy extends LocalStrategy {
async getEntityQuery(query: Query) {
return {
...query,
active: true,
$limit: 1
};
}
}
Extend the JWT strategy by alter getEntity() to return null if the user is inactive
class CustomJWTStrategy extends JWTStrategy {
async getEntity(id: Id) {
const entity = await this.entityService.get(id);
if (!entity.active) {
return null;
}
return entity;
}
}
export default function(app: Application): void {
const authentication = new AuthenticationService(app);
authentication.register('jwt', new CustomJWTStrategy());
authentication.register('local', new CustomLocalStrategy());
app.use('/authentication', authentication);
app.configure(expressOauth());
}

Resources