I'm passing a JWT from my client application (nextJS with nextAuth) via credentials header to my backend nestJS application (which is using graphQL). In my nestJS backend application I'm trying to implement an auth guard, so I extract the JWT with a custom function in my jwt.strategy.ts
But the JwtStrategy is not accepting my valid signed token. To prove that the JWT is valid, I put some console output for the token. But the validate() function is never called. I do not understand why, as the token can be validated with jwt.verify:
This is my output - it gets decoded by the jwt.verify():
JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6MTIzLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaXNBZG1pbiI6dHJ1ZX0sImlhdCI6MTYwOTY3NTc4Nn0.LQy4QSesxJR91PyGGb_0mGZjpw9hlC4q7elIDs2CkLo
Secret: uGEFpuMDDdDQA3vCtZXPKgBYAriWWGrk
Decoded: {
user: { userId: 123, username: 'username', isAdmin: true },
iat: 1609675786
}
I don't see, what I am missing and I even do not see how to debug it as there is no output in my jwt.strategy.ts and the validate-function is not called at all.
jwt.strategy.ts
import jwt from 'jsonwebtoken'
// import { JwtService } from '#nestjs/jwt'
import { Strategy } from 'passport-jwt'
import { PassportStrategy } from '#nestjs/passport'
import { Injectable } from '#nestjs/common'
import cookie from 'cookie'
import { getConfig } from '#myapp/config'
const { secret } = getConfig()
const parseCookie = (cookies) => cookie.parse(cookies || '')
const cookieExtractor = async (req) => {
let token = null
if (req?.headers?.cookie) {
token = parseCookie(req.headers.cookie)['next-auth.session-token']
}
// output as shown above
console.log('JWT:', token)
console.log('Secret:', secret)
const decoded = await jwt.verify(token, secret, { algorithms: ['HS256'] })
console.log('Decoded: ', decoded)
return token
}
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: cookieExtractor,
ignoreExpiration: true,
secretOrKey: secret
})
}
async validate(payload: any) {
console.log('payload:', payload) // is never called
return { userId: payload.sub, username: payload.username }
}
}
jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '#nestjs/common'
import { AuthGuard } from '#nestjs/passport'
import { GqlExecutionContext } from '#nestjs/graphql'
#Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: GqlExecutionContext) {
const ctx = GqlExecutionContext.create(context)
return ctx.getContext().req
}
}
The guard is used in this resolver:
editor.resolver.ts
import { Query, Resolver } from '#nestjs/graphql'
import { UseGuards } from '#nestjs/common'
import { GqlAuthGuard } from '../auth/jwt-auth.guard'
#Resolver('Editor')
export class EditorResolvers {
constructor(private readonly editorService: EditorService) {}
#UseGuards(GqlAuthGuard)
#Query(() => [File])
async getFiles() {
return this.editorService.getFiles()
}
}
auth.module.ts
import { Module } from '#nestjs/common'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { PassportModule } from '#nestjs/passport'
import { LocalStrategy } from './local.strategy'
import { JwtStrategy } from './jwt.strategy'
import { UsersModule } from '../users/users.module'
import { JwtModule } from '#nestjs/jwt'
import { getConfig } from '#myApp/config'
const { secret } = getConfig()
#Module({
imports: [
UsersModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret,
verifyOptions: { algorithms: ['HS256'] },
signOptions: { expiresIn: '1d' }
})
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, LocalStrategy],
exports: [AuthService]
})
export class AuthModule {}
The token is created on server side (nextJS api page) with:
const encode = async ({ secret, token }) => jwt.sign(token, secret, { algorithm: 'HS256' })
I see 2 differences from nestJS docs examples from your jwt.strategy.ts file that you can change and give it a try..
https://docs.nestjs.com/security/authentication#implementing-passport-jwt
sync extractor and not async
By default passport-jwt extractor we can see that is an sync and not async, so you can try change your extractor and remove the async, or to add await when calling it.
https://github.com/mikenicholson/passport-jwt/blob/master/lib/extract_jwt.js ,
look for fromAuthHeaderAsBearerToken function.
so or change your
const cookieExtractor = async (req) => {
to
const cookieExtractor = (req) => {
OR - add await when you call it
jwtFromRequest: await cookieExtractor(),
call the extractor and not just pass it
by the docs example in JwtStrategy constructor they calling the extractor and not passing it like you do
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
so try to call it in your JwtStrategy constructor
jwtFromRequest: cookieExtractor(), // (again - take care of sync / async)
Related
I have this basic CRUD methods in Nestjs.
The issue I am facing is that when I am applying the getCurrentUserId() method on top on all methods it works fine but when I am applying in bottom it doesnt work and gives error.
Is there anything wrong with middleware ?
user.controller.ts
#Controller('users')
#Serialize(UserDto)
export class UsersController {
constructor(private usersService: UsersService) {}
#Post('/signup')
create(#Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
#Get('/#:userName')
async getUserByUsername(#Param('userName') userName: string) {
const user = await this.usersService.findByName(userName);
console.log(userName);
if (!user) {
throw new NotFoundException('User Not Found');
}
return user;
}
//! Testing for current user
#Get('/current')
#UseGuards(JwtAuthGuard)
async getCurrentUserId(#CurrentUser() id: string) {
console.log('running endpoint');
return id;
}
}
current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '#nestjs/common';
export const CurrentUser = createParamDecorator(
(data : unknown , context : ExecutionContext) => {
const req = context.switchToHttp().getRequest();
console.log("I am running")
return req.id;
}
)
current-user.middleware.ts
#Injectable()
export class CurrentUserMiddleware implements NestMiddleware {
constructor(private usersService: UsersService) {}
async use(req: RequestId, res: Response, next: NextFunction) {
const token = req.headers['authorization'];
console.log(token);
if (!token) {
throw new UnauthorizedException('Unauthorized');
}
try {
const { userId } =
await this.usersService.getUserByToken(token);
req.id = userId;
console.log(req.id)
next();
} catch {
throw new UnauthorizedException();
}
}
}
And I have added the middleware to user.module.ts like this
export class UsersModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(CurrentUserMiddleware).forRoutes(
'users/current'
);
}
}
The route is matching on #Get('/#:userName') before it makes it to #Get('/current') so its executing the code inside of your getUserByUsername method instead.
Just move getCurrentUserId to the top and you should be fine.
Routes are evaluated in the order they are defined and the first matching one is used to handle the request. In general you should always put the most specific routes (the ones without route params) at the top of your controller to avoid this problem.
Learning GraphQL and a bit stuck with passing req into a generate context function I made to keep things neat.
I think I am doing something dumb with the line createContext((req) => req) as if I console.log( ctx.request ) in a route handler I get [Function (anonymous)]
Whats an alternative way to capture the this.req from the server.ts scope and pass it into createContext?
server.ts
import { ApolloServer } from 'apollo-server'
import { schema } from './nexusSchema'
import { createContext } from './context'
const server = new ApolloServer({
schema,
context: createContext((req) => req),
})
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
})
context.ts
import { PrismaClient, Prisma as PrismaTypes } from '#prisma/client'
import { PrismaDelete, onDeleteArgs } from '#paljs/plugins'
import { PubSub } from 'apollo-server'
class Prisma extends PrismaClient {
constructor(options?: PrismaTypes.PrismaClientOptions) {
super(options)
}
async onDelete(args: onDeleteArgs) {
const prismaDelete = new PrismaDelete(this)
await prismaDelete.onDelete(args)
}
}
const prisma = new Prisma()
const pubsub = new PubSub()
export interface Context {
prisma: Prisma
select: any
pubsub: PubSub
request: {
request: {
headers: {
authorization: string
}
}
connection: {
context: {
Authorization: string
}
}
}
}
export function createContext(req): Context {
return {
prisma,
select: {},
pubsub,
request: req,
}
}
I was being dumb after all.
Note, I also had to adjust the interface for the request object to suit Apollo Server 2!
server.ts
const server = new ApolloServer({
schema,
context: createContext,
})
context.ts
export interface Context {
prisma: Prisma
select: any
pubsub: PubSub
request: {
req: {
headers: {
authorization: string
}
}
connection: {
context: {
Authorization: string
}
}
}
}
export const createContext = (req): Context => {
return {
prisma,
select: {},
pubsub,
request: req,
}
}
I made HttpExceptionFilter as below in nestjs.
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '#nestjs/common';
import { Response } from 'express';
#Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception, host: ArgumentsHost) {
const context = host.switchToHttp();
const response = context.getResponse<Response>();
const status = (exception.getStatus && exception.getStatus()) || 500;
response.status(status).json({
code: status,
success: false,
});
}
}
And I put it into app.module to use it globally.
#Module({
imports: [
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
So far, it works very well except pipe of nestjs.
I made a pipe and made it with #UsePipes in other controller.
This is my pipe code.
import { ArgumentMetadata, Injectable, PipeTransform } from '#nestjs/common';
#Injectable()
export class SplitEmailPipe implements PipeTransform<string, string> {
transform(value: any, metadata: ArgumentMetadata): any {
let email = '';
try {
email = value.split('#')[1];
} catch (err) {
throw new Error(err);
}
return { email };
}
}
And I put that pipe using #UsePipes.
Pipe works well in this case.
#Post('/')
#UsePipes(new SplitEmailPipe())
public async signIn(
#Res() res,
#Body() signInDto: SignInDto,
) {
... do something
}
But the problem is HttpExceptionFilter doesn't work. It response by default response of nestjs.
Could you give me some advice for this problem?
That happens because you're not throwing an error from type HttpException, to fix this issue you should to replace :
import { ArgumentMetadata, Injectable, PipeTransform } from '#nestjs/common';
#Injectable()
export class SplitEmailPipe implements PipeTransform<string, string> {
transform(value: any, metadata: ArgumentMetadata): any {
let email = '';
try {
email = value.split('#')[1];
} catch (err) {
throw new BadRequestException(err);
}
return { email };
}
}
I am trying to create a test case for a service in angular6. The service has a bunch of different http request methods (get, put, post etc) and within them an API call is made which fetches the appropriate response. I'm trying to create the test cases where a mock http request is made and a response is returned. However, I have followed a Tutorial which apparently helps me do exactly what I want.
However, when I run the test case for the service it gives me the following error (I've censored the URL in GET for privacy purposes:
Error: Expected no open requests, found 1: GET https://staging.xxxxxxxxxx.co.uk/rest/v11_1/oauth2/token
at HttpClientTestingBackend.push../node_modules/#angular/common/fesm5/http/testing.js.HttpClientTestingBackend.verify (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/common/fesm5/http/testing.js:326:1)
at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/webpack:/src/app/Services/adapter.service.spec.ts:22:13)
at TestBed.push../node_modules/#angular/core/fesm5/testing.js.TestBed.execute (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/fesm5/testing.js:1073:1)
at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/fesm5/testing.js:1224:29)
at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone.js:388:1)
at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone-testing.js:288:1)
at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone.js:387:1)
at Zone.push../node_modules/zone.js/dist/zone.js.Zone.run (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone.js:138:1)
at runInTestZone (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone-testing.js:509:1)
at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone-testing.js:524:1)
I've tried browsing through This solution as well as This one, but to no avail.
Here is the code for my service:
import { Injectable } from '#angular/core';
import { environment } from '../../environments/environment';
import {
HttpHeaders,
HttpClient,
HttpParams,
} from '#angular/common/http';
import { Request, RequestOptions, Headers } from '#angular/http';
import { Observable } from 'rxjs/Rx';
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import { JwtService } from './jwt.service';
const API_URL = environment.api.host;
#Injectable({
providedIn: 'root'
})
export class AdapterService {
constructor(private http: HttpClient, private jwtService: JwtService) {}
private formatErrors(self: AdapterService) {
return (res: Response) => {
return Observable.throw(res);
};
}
private requestHeaders(path: string) {
let headers;
if (path !== 'oauth2/token') {
headers = new HttpHeaders({
'Accept': 'application/json',
'Oauth-Token': this.jwtService.getToken()
})
}
return headers;
}
get(path: string, params: HttpParams = new HttpParams()): Observable < any > {
let headers = this.requestHeaders(path);
return this.http.get(`${API_URL}${path}`, { headers })
.catch(catchError(this.formatErrors(this)));
}
put(path: string, body: Object = {}): Observable < any > {
return this.http.put(
`${API_URL}${path}`,
JSON.stringify(body),
).catch(catchError(this.formatErrors(this)));
}
post(path: string, body: Object = {}): Observable < any > {
return this.http.post(
`${API_URL}${path}`,
JSON.stringify(body),
).catch(catchError(this.formatErrors(this)));
}
delete(path): Observable < any > {
return this.http.delete(
`${API_URL}${path}`,
).catch(catchError(this.formatErrors(this)));
}
}
The Test Case:
import { TestBed, async, inject } from '#angular/core/testing';
import { HttpClientModule, HttpRequest, HttpParams } from '#angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '#angular/common/http/testing';
import { AdapterService } from './adapter.service';
describe('AdapterService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
HttpClientTestingModule
],
providers: [
AdapterService
]
});
});
afterEach(inject([HttpTestingController], (backend: HttpTestingController) => {
backend.verify();
}));
it('should send a valid get request for token', async(inject([AdapterService, HttpTestingController],
(service: AdapterService, backend: HttpTestingController) => {
service.get('oauth2/token').subscribe((next)=>{
expect(next).toBeDefined();
});
})));
// it('')
});
SOLVED I forgot to add an expectOne request for the API call within the test case:
backend.expectOne( API_URL + 'oauth2/token').flush(null, { status: 200, statusText:'Ok' });
A very naive observation, apologies for the inconvenience.
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from './auth.service';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { JwtPayload } from './model/jwt-payload.model';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'secretKey',
});
}
async validate(payload: JwtPayload) {
const user = await this.authService.validateUser(payload);
if (!user) {
throw new UnauthorizedException();
}
return true;
}
}
Token is extracted from the request by PassportStrategy. I don't know how to catch the error when the token expires or gets invalid. My purpose is if there is an error because the token expired, I need to refresh the token. Otherwise do something else.
Refresh token implementation could be handled in canActivate method in custom auth guard.
If the access token is expired, the refresh token will be used to obtain a new access token. In that process, refresh token is updated too.
If both tokens aren't valid, cookies will be cleared.
#Injectable()
export class CustomAuthGuard extends AuthGuard('jwt') {
private logger = new Logger(CustomAuthGuard.name);
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
try {
const accessToken = ExtractJwt.fromExtractors([cookieExtractor])(request);
if (!accessToken)
throw new UnauthorizedException('Access token is not set');
const isValidAccessToken = this.authService.validateToken(accessToken);
if (isValidAccessToken) return this.activate(context);
const refreshToken = request.cookies[REFRESH_TOKEN_COOKIE_NAME];
if (!refreshToken)
throw new UnauthorizedException('Refresh token is not set');
const isValidRefreshToken = this.authService.validateToken(refreshToken);
if (!isValidRefreshToken)
throw new UnauthorizedException('Refresh token is not valid');
const user = await this.userService.getByRefreshToken(refreshToken);
const {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
} = this.authService.createTokens(user.id);
await this.userService.updateRefreshToken(user.id, newRefreshToken);
request.cookies[ACCESS_TOKEN_COOKIE_NAME] = newAccessToken;
request.cookies[REFRESH_TOKEN_COOKIE_NAME] = newRefreshToken;
response.cookie(ACCESS_TOKEN_COOKIE_NAME, newAccessToken, COOKIE_OPTIONS);
response.cookie(
REFRESH_TOKEN_COOKIE_NAME,
newRefreshToken,
COOKIE_OPTIONS,
);
return this.activate(context);
} catch (err) {
this.logger.error(err.message);
response.clearCookie(ACCESS_TOKEN_COOKIE_NAME, COOKIE_OPTIONS);
response.clearCookie(REFRESH_TOKEN_COOKIE_NAME, COOKIE_OPTIONS);
return false;
}
}
async activate(context: ExecutionContext): Promise<boolean> {
return super.canActivate(context) as Promise<boolean>;
}
handleRequest(err, user) {
if (err || !user) {
throw new UnauthorizedException();
}
return user;
}
}
Attaching user to the request is done in validate method in JwtStrategy class, it will be called if the access token is valid
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
readonly configService: ConfigService,
private readonly userService: UserService,
) {
super({
jwtFromRequest: cookieExtractor,
ignoreExpiration: false,
secretOrKey: configService.get('jwt.secret'),
});
}
async validate({ id }): Promise<User> {
const user = await this.userService.get(id);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Example for custom cookie extractor
export const cookieExtractor = (request: Request): string | null => {
let token = null;
if (request && request.signedCookies) {
token = request.signedCookies[ACCESS_TOKEN_COOKIE_NAME];
}
return token;
};
Instead of using the built-in AuthGuard you can create your own one and overwrite the request handler:
#Injectable()
export class MyAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info: Error) {
if (info instanceof TokenExpiredError) {
// do stuff when token is expired
console.log('token expired');
}
return user;
}
}
Depending on what you want to do, you can also overwrite the canActivate method where you have access to the request object. Have a look at the AuthGuard sourcecode.