Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using 'nestjs/jwt' signing with dynamic/user-related secret

I'm trying to create a user token based on the secret of the user trying to log in. However instead of using a secret from the environment I want to use a secret assigned to a user object inside the database.

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '@src/modules/user/services';

@Injectable()
export class AuthService {
  public constructor(private readonly jwtService: JwtService,
                     private readonly userService: UserService) {}

  public async createToken(email: string): Promise<JwtReply> {
    const expiresIn = 60 * 60 * 24;
    const user = await this.userService.user({ where: { email } });
    const accessToken = await this.jwtService.signAsync({ email: user.email },
                                                        /* user.secret ,*/
                                                        { expiresIn });

    return {
      accessToken,
      expiresIn,
    };
  }
}

I'm new to Nestjs and maybe I'm missing something. node-jsonwebtoken does provide the necessary parameter in the sign(...) function. nestjs/jwt is missing this parameter (see code). How would you solve it without using node-jsonwebtoken or maybe a more abstract question: does my way of handling user secret make sense here? Thanks.

like image 493
Tom Siwik Avatar asked Mar 26 '19 09:03

Tom Siwik


2 Answers

This is not yet possible solely with nest's JwtModule but you can easily implement the missing parts yourself.

Live Demo

Edit Nest Dynamic JWT Secrets

You can create tokens by calling the following routes:

user1 (secret: '123'): https://yw7wz99zv1.sse.codesandbox.io/login/1
user2 (secret: '456'): https://yw7wz99zv1.sse.codesandbox.io/login/2

Then call the protected route '/' with your token and receive your user:

curl -X GET https://yw7wz99zv1.sse.codesandbox.io/ \
      -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxIiwiaWF0IjoxNTUzNjQwMjc5fQ.E5o3djesqWVHNGe-Hi3KODp0aTiQU9X_H3Murht1R5U'

How does it work?

In the AuthService I'm just using the standard jsonwebtoken library to create the token. You can then call createToken from your login route:

import * as jwt from 'jsonwebtoken';

export class AuthService {
  constructor(private readonly userService: UserService) {}

  createToken(userId: string) {
    const user = this.userService.getUser(userId);
    return jwt.sign({ userId: user.userId }, user.secret, { expiresIn: 3600 });
  }

  // ...
}

In the JwtStrategy you use secretOrKeyProvider instead of secretOrKey which can asynchronously access the UserService to get the user secret dynamically:

export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    private readonly authService: AuthService,
    private readonly userService: UserService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKeyProvider: (request, jwtToken, done) => {
        const decodedToken: any = jwt.decode(jwtToken);
        const user = this.userService.getUser(decodedToken.userId);
        done(null, user.secret);
      },
    });
  }

  // ...
}

Note that the options you pass to the JwtModule like expiresIn will not be used, instead directly pass your options in the AuthService. Import the JwtModule without any options:

JwtModule.register({})

General

Does my way of handling user secret make sense here?

This is hard to answer without knowing your exact requirements. I guess there are use cases for jwt with dynamic secrets but with it you are losing a great property of jwt: they are stateless. This means that your AuthService can issue a jwt token and some ProductService that requires authentication can just trust the jwt (it knows the secret) without making any calls to other services (i.e. UserService which has to query the database).

If user-related keys are not a hard requirement consider rotating the keys frequently instead by making use of jwt's kid property.

like image 116
Kim Kern Avatar answered Nov 16 '22 03:11

Kim Kern


The option to add secret into JwtSignOptions has been added in nestjs/jwt version 7.1.0.

With that, the example would be:

public async createToken(email: string): Promise<JwtReply> {
    const expiresIn = 60 * 60 * 24;
    const user = await this.userService.user({ where: { email } });
    const accessToken = await this.jwtService.signAsync(
        { email: user.email },
        { expiresIn,
          secret: user.secret,
        });

    return {
      accessToken,
      expiresIn,
    };
 }


like image 26
hsteamroller Avatar answered Nov 16 '22 04:11

hsteamroller