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());
}
Related
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");
I have this code to change the default message from typeorm when a value in a unique column already exists. It just creates a custom message when we get an error 23505.
if (error.code === '23505') {
// message = This COLUMN VALUE already exists.
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
throw new BadRequestException(message);
}
throw new InternalServerErrorException();
I will have to use it in other services, so I would like to abstract that code.
I think I could just create a helper and then I import and call it wherever I need it. But I don’t know if there is a better solution to use it globally with a filter or an interceptor, so I don’t have to even import and call it in different services.
Is this possible? how can that be done?
If it is not possible, what do you think the best solution would be?
Here all the service code:
#Injectable()
export class MerchantsService {
constructor(
#InjectRepository(Merchant)
private merchantRepository: Repository<Merchant>,
) {}
public async create(createMerchantDto: CreateMerchantDto) {
try {
const user = this.merchantRepository.create({
...createMerchantDto,
documentType: DocumentType.NIT,
isActive: false,
});
await this.merchantRepository.save(user);
const { password, ...merchantData } = createMerchantDto;
return {
...merchantData,
};
} catch (error) {
if (error.code === '23505') {
// message = This COLUMN VALUE already exists.
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
throw new BadRequestException(message);
}
throw new InternalServerErrorException();
}
}
public async findOneByEmail(email: string): Promise<Merchant | null> {
return this.merchantRepository.findOneBy({ email });
}
}
I created an exception filter for typeORM errors.
This was the result:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpStatus,
InternalServerErrorException,
} from '#nestjs/common';
import { Response } from 'express';
import { QueryFailedError, TypeORMError } from 'typeorm';
type ExceptionResponse = {
statusCode: number;
message: string;
};
#Catch(TypeORMError, QueryFailedError)
export class TypeORMExceptionFilter implements ExceptionFilter {
private defaultExceptionResponse: ExceptionResponse =
new InternalServerErrorException().getResponse() as ExceptionResponse;
private exceptionResponse: ExceptionResponse = this.defaultExceptionResponse;
catch(exception: TypeORMError | QueryFailedError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
exception instanceof QueryFailedError &&
this.setQueryFailedErrorResponse(exception);
response
.status(this.exceptionResponse.statusCode)
.json(this.exceptionResponse);
}
private setQueryFailedErrorResponse(exception: QueryFailedError): void {
const error = exception.driverError;
if (error.code === '23505') {
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
this.exceptionResponse = {
statusCode: HttpStatus.BAD_REQUEST,
message,
};
}
// Other error codes can be handled here
}
// Add more methods here to set a different response for any other typeORM error, if needed.
// All typeORM erros: https://github.com/typeorm/typeorm/tree/master/src/error
}
I set it globally:
import { TypeORMExceptionFilter } from './common';
async function bootstrap() {
//...Other code
app.useGlobalFilters(new TypeORMExceptionFilter());
//...Other code
await app.listen(3000);
}
bootstrap();
And now I don't have to add any code when doing changes in the database:
#Injectable()
export class MerchantsService {
constructor(
#InjectRepository(Merchant)
private merchantRepository: Repository<Merchant>,
) {}
public async create(createMerchantDto: CreateMerchantDto) {
const user = this.merchantRepository.create({
...createMerchantDto,
documentType: DocumentType.NIT,
isActive: false,
});
await this.merchantRepository.save(user);
const { password, ...merchantData } = createMerchantDto;
return {
...merchantData,
};
}
}
Notice that now I don't use try catch because nest is handling the exceptions. When the repository save() method returns an error (actually it is a rejected promise), it is caught in the filter.
I am building out the user auth portion of a nextjs app. I have most of it completed save for the protected routes. I am attempting to handle the auth portion in _app.js. Here is the file:
import App from 'next/app'
import '../styles/globals.css'
import { parseCookies } from 'nookies'
import { redirectUser } from '../utils/auth'
import jwt from 'jsonwebtoken'
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
const { token } = parseCookies(ctx)
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
if (!token) {
const isProtectedRoute = ctx.pathname === '/account'
if (isProtectedRoute) {
redirectUser(ctx, '/login')
}
} else {
try {
const verified = jwt.verify(token, process.env.JWT_SECRET)
} catch(err) {
console.error(err)
redirectUser(ctx, '/login')
}
}
return { pageProps }
}
render() {
const { Component, pageProps } = this.props;
return (
<Component {...pageProps} />
)
}
}
export default MyApp
And my redirectUser file:
import cookie from 'js-cookie';
import Router from 'next/router';
export function handleLogin(token) {
cookie.set('token', token)
Router.push('/account');
}
export function redirectUser(ctx, location) {
if (ctx.req) {
ctx.res.writeHead(302, { Location: location })
ctx.res.end()
} else {
Router.push(location)
}
}
The auth portion correctly redirects when a user doesn't have a token but if that token is altered to be invalid I get a redirect error:
The page isn’t redirecting properly
Firefox has detected that the server is redirecting the request for this address in a way that will never complete.
I've confirmed that it is the second redirectUser call that is causing this error. Even if I place it outside of the else conditional at the bottom of the getInitialProps function I get the same result.
You're getting that error because you end up triggering an infinite redirect cycle to the /login page.
Landing on any page with an invalid token will redirect to the /login page. When this redirect happens it will trigger another App.getInitialProps call which will end up causing yet another redirect to the /login page since the token is still invalid, and so on.
You could prevent this behaviour by adding checks when the current page is the /login page. This can be handled in several ways, but I'll leave an example here.
if (!token) {
const isProtectedRoute = ctx.pathname === '/account'
if (isProtectedRoute) {
redirectUser(ctx, '/login')
}
} else if (ctx.pathname !== "/login") { // To avoid redirect loop
try {
const verified = jwt.verify(token, process.env.JWT_SECRET)
} catch(err) {
console.error(err)
redirectUser(ctx, '/login')
}
}
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
I'm trying to create an application with NestJS framework and I'd like to check if a specific Service is in the application context but, the framework default behavior is exiting the process when it doesn't find the Service inside the context.
I've surrounded the get method with try...catch but it doesn't work because of the process.exit(1) execution.
private async loadConfigService(server: INestApplication): Promise<void> {
try {
this.configService = await server.get<ConfigService>(ConfigService, { strict: false });
} catch (error) {
this.logger.debug('Server does not have a config module loaded. Loading defaults...');
}
this.port = this.configService ? this.configService.port : DEFAULT_PORT;
this.environment = this.configService ? this.configService.environment : Environment[process.env.NODE_ENV] || Environment.DEVELOPMENT;
this.isProduction = this.configService ? this.configService.isProduction : false;
}
I'd like to catch the exception to manage the result instead of exiting the process.
Here's what I came up with:
import { NestFactory } from '#nestjs/core';
export class CustomNestFactory {
constructor() {}
public static create(module, serverOrOptions, options) {
const ob = NestFactory as any;
ob.__proto__.createExceptionZone = (receiver, prop) => {
return (...args) => {
const result = receiver[prop](...args);
return result;
};
};
return NestFactory.create(module, serverOrOptions, options);
}
}
Now, instead of using the default NestFactory I can use my CustomNestFactory
Dirty, but it works
Here is my code to solve same issue:
import { ExceptiinsZone } from '#nestjs/core/errors/exceptions-zone';
ExceptionsZone.run = callback => {
try {
callback();
} catch (e) {
ExceptionsZone.exceptionHandler.handle(e);
throw e;
}
};
You may need to override asyncRun() also for other method.