The example repo can be found: GitHub - dabiddo/laravel-nestjs-auth: Laravel
At work, we are migrating a big laravel monolith app to microservices, our PM chose NestJs since most of the team has a bit of experience using it, and one of the big questions was, how do we migrate the current users to the new architecture, since the users
table does not change much, we decided to leave it, and make NestJs accept the Laravel JWT for Microservice authentication.
And so, a week of research and development led to this post, where I summarize what we did at the Backend team to make it work.
Since this will focus on the Auth part, I’ll summarize the laravel-passport tutorial, for that, you can follow the Laravel Passport Official Docs or follow this Laravel 10 Passport API Authentication Tutorial, from which I copy-pasted the Laravel code.
make a basic Larave Project and Install laravel-passport, for generating JWT Token
composer create-project laravel/laravel example-app
Install Passport
composer require laravel/passport
Run Migrations
php artisan migrate
Install Passport
php artisan passport:install
This will generate 2 Keys inside the storage/
folder.
oauth-private.key
oauth-public.key
For the next part, you will also need to create a NestJs project and install the passport library for jwt generation and authentication.
$ npm i -g @nestjs/cli
$ nest new project-name
Before you continue, please read the official NestJs Authentication tutorial, and follow up with the NestJs-Passport tutorial to have an idea of all the packages needed for the tutorial.
$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
If you followed the Tutorial, you will have a jwt.strategy.ts
inside the auth
folder of your NestJs, this is the file we will need to modify to accept the RS256 encryption.
Since Laravel and NestJs will share the same DB for extracting the user data, I used TypeOrm since its the one the NestJs tutorials use, but feel free to change it if you want.
npm install --save @nestjs/typeorm typeorm mysql2
$ npm i --save @nestjs/config
$ npm i jwks-rsa
$ npm i bcrypt
$ npm i -D @types/bcrypt
If you generate a JWT token from laravel, and copy-paste it to jwt.io, you will the the algorithm detected is the RS256
, which uses the private & public keys to encrypt and decrypt the JWT to get the payload information.
Knowing that information, we need to make NestJs Passport, accept the same algorithm and share the keys in order to encrypt / decrypt the same payload.
There are 2 important files that need to be changed in order for NestJs to accept Laravel’s JWT Token jwt-auth.guard.ts
& jwt.strategy.ts
JWT Guard
This is the file incharge of decrypting the JWT token using the Public Key, once it can be decrypted, it will inject the result payload to the request.
import { AuthGuard } from '@nestjs/passport';
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';
import * as fs from 'fs';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private readonly jwtService: JwtService,
private readonly reflector: Reflector,
) {
super({
jwtOptions: {
algorithms: ['RS256'],
},
});
}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
try {
const token = request.headers.authorization.split(' ')[1];
const publicKey = fs.readFileSync('/app/laravel-auth/storage/oauth-public.key');
const payload = this.jwtService.verify(token, {
secret: publicKey,
algorithms: ['RS256']
});
request.user = payload;
return true;
} catch (error) {
console.log(error);
throw new UnauthorizedException('Invalid token');
}
}
}
JWT Strategy
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import * as jwksRsa from 'jwks-rsa';
import jwt, { Secret, GetPublicKeyOrSecret } from 'jsonwebtoken';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
algorithms: ['RS256'],
secretOrKeyProvider: (
request: any,
rawJwtToken: string,
done: (err: any, secretOrPublicKey?: string | object) => void,
) => {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
return done('Invalid token');
}
const { header } = decodedToken;
const { alg, kid } = header;
const jwksClient = jwksRsa({
jwksUri: 'http://localhost/',
});
jwksClient.getSigningKey(kid, (err, key) => {
if (err) {
return done(err);
}
const signingKey = key.getPublicKey();
done(null, signingKey);
});
},
});
}
}
Once you have this files modified, you should be able to generate a token from Laravel and access the NestJs protected routes.
It should be posible to generate tokens from NestJs that laravel can accept, but I have’t been able to do it, this is what I have so Far
AuthController
@Post('login')
async login(@Body() req)
{
const response = await this.usersService.findOneByEmail(req.email);
const hash = response.password.replace(/^\$2y(.+)$/i, '$2a$1');
const match = await bcrypt.compare(req.password, hash);
if(match === true) {
const payload = { username: response.name, email: response.email, sub: response.id };
const privateKey = fs.readFileSync('/app/laravel-auth/storage/oauth-private.key');
return {
access_token: jwt.sign(payload, privateKey, { algorithm: 'RS256', expiresIn: '1h' })
};
}
else {
throw new UnauthorizedException();
}
}
I need to do the replace, because both Laravel and NestJs hash the passwords a bit different, so I keep getting the Incorrect password when comparing.
So far it generates a Jwt token, and it does work on NestJs routes, but not on Laravel.