#NestJS JWT Authorization Not Working with Supabase Guard

29 messages · Page 1 of 1 (latest)

cloud ember
#

Context

I am working on a NestJS application where I use Supabase authentication with JWT to protect certain API endpoints. I am using passport-jwt for authentication and @nestjs/passport for the guard. The goal is to restrict access to the /protected endpoint so that only users with a valid JWT can access it.

Setup

  • Authentication flow:

    • Users sign in via POST /auth/sign-in, which returns a JWT.
    • This token is then included in the Authorization header (Bearer <token>) when making a request to the GET /protected endpoint.
    • The SupabaseJwtAuthGuard is applied to the protected endpoint to enforce authentication.
  • Code structure:

    • SupabaseJwtAuthGuard extends AuthGuard('jwt')
    • SupabaseStrategy uses passport-jwt to extract the token from the Authorization header.
    • The JWT secret is loaded via ConfigService (configService.get<string>('JWT_SECRET_KEY')).
#

The Problem

  • When I make a request to GET /protected with a valid JWT in the Authorization header, I get the following response:
    {
      "statusCode": 401,
      "message": "Unauthorized"
    }
    
  • This happens even though:
    • The token is properly included in the request headers.
    • The token was issued successfully via the sign-in endpoint.
    • The SupabaseJwtAuthGuard is applied correctly to the route.
#

Controller with Protected Route

@Get('protected')
@UseGuards(SupabaseJwtAuthGuard)
async protected(@Req() req) {
  return {
    message: 'AuthGuard works 🎉',
    authenticated_user: req.user,
  };
}

Supabase JWT Guard

import { AuthGuard } from '@nestjs/passport';

export class SupabaseJwtAuthGuard extends AuthGuard('jwt') {}

JWT Strategy (passport-jwt)

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class SupabaseStrategy extends PassportStrategy(Strategy) {
  public constructor(private readonly configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_SECRET_KEY'),
    });
  }

  async validate(payload: any): Promise<any> {
    return payload;
  }
}

Auth Module

@Module({
  imports: [
    ConfigModule,
    JwtModule.registerAsync({
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET_KEY'),
        signOptions: { expiresIn: 40000 },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [SupabaseStrategy],
  exports: [SupabaseStrategy],
})
export class AuthModule {}
#

Debugging Steps I’ve Taken

  1. Checked if the JWT token is present in the request headers

    • Logged req.headers inside protected() and confirmed that the Authorization header is set correctly.
  2. Verified the JWT contents

    • Decoded the JWT using jwt.decode(token) and verified that it contains valid claims (sub, iat, exp).
  3. Ensured JWT_SECRET_KEY is being loaded

    • Logged configService.get<string>('JWT_SECRET_KEY') inside SupabaseStrategy and confirmed it is correctly set.
  4. Checked if validate(payload) is being called

    • Added a console.log(payload) inside validate() in SupabaseStrategy, but it never logs anything, indicating that Passport is not calling it.
  5. Confirmed the guard is executing

    • Added a console.log('Authorization Header:', req.headers.authorization) inside SupabaseJwtAuthGuard.canActivate(), and it logs correctly.
  6. Checked if AuthModule is correctly imported in SupabaseModule

    • Verified that AuthModule is imported in SupabaseModule, where the protected route exists.
#

Remaining Questions

  • Why is passport-jwt not calling the validate(payload) method?
  • Is there an issue with how I configured passport-jwt in SupabaseStrategy?
  • Could it be related to how NestJS modules are handling dependencies?

Would appreciate any insights into what might be going wrong. Thanks in advance! 🚀

cloud ember
#

I think the supabase guard is unable to verify the jwt token correctly.

keen charm
#

Some ideas to further troubleshoot:

  • Turn off expiration validation with ignoreExpires: true to be sure your not having a timing issue.
  • Console log what you get out of ExtractJwt.fromAuthHeaderAsBearerToken() to make sure the token can be pulled properly from the request header.
cloud ember
#

Nothing is logged

cloud ember
#

I am logging it inside the authenticate method tho

#

the token is displayed correctly

#

Using the supabase anon key instead of my own JWT Secret also does not work

#

I finally got it to work

#

I was using the Service role key instead of the Actual jwt secret from the supabase project

cloud ember
#

!solved

keen charm
verbal wingBOT
#

This post has been marked as resolved. ✅
Please read through the conversation and resolution, if you are having the same issue.
If you were the original author of the post and the issue is still fresh (within a few days) and you are still have having trouble, continue to reply here. If you are not the original author of the post or the post has aged, start a new thread linking this one as relevant to your problem, providing as much additional information as possible.

vocal hearth
cloud ember
#

Hey

#

Inside the auth settings of your supabase project you should look for the JWT secret.

#

Use that one for auth

#

the env var is named: SUPABASE_JWT_SECRET_SECRET

#

in mine project

#

And it's being used inside the auth.module.ts file: ```ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { HttpModule } from '@nestjs/axios';
import { SessionsModule } from '../sessions/sessions.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UserService } from '../modules/user/user.service';
import { PrismaService } from '../modules/prisma/prisma.service';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { SupabaseStrategy } from './strategies/supabase.strategy';

@Module({
imports: [
HttpModule,
SessionsModule,
ConfigModule,
JwtModule.registerAsync({
useFactory: (configService: ConfigService) => {
return {
global: true,
secret: configService.get<string>('SUPABASE_JWT_SECRET_SECRET'),
signOptions: { expiresIn: '2h' },
};
},
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [
AuthService,
UserService,
PrismaService,
JwtService,
SupabaseStrategy,
],
exports: [AuthService],
})
export class AuthModule {}

#

And the supabase.strategy.ts file: ```ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class SupabaseStrategy extends PassportStrategy(Strategy) {
public constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>('SUPABASE_JWT_SECRET_SECRET'),
ignoreExpiration: false,
});
}

async validate(payload: any): Promise<any> {
return payload;
}

authenticate(req) {
super.authenticate(req);
}
}

vocal hearth
#

thanks, so you have envs defined in Supabase, I have it defined locally - just testing connection

#
  imports: [
    ConfigModule,
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get('JWT_SECRET'),
        signOptions: {
          expiresIn: configService.get('JWT_EXPIRATION_TIME'),
        },
      }),
    }),
    TypeOrmModule.forFeature([UserRepository]),
  ],
  controllers: [AuthController],
  providers: [AuthService, UserRepository, JwtStrategy],
  exports: [JwtStrategy, PassportModule],
})
export class AuthModule {}
#

and inside the strategy class:

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    private userRepository: UserRepository,
    private configService: ConfigService
  ) {
    super({
      secretOrKey: configService.get('JWT_SECRET') as string,
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    });    
  }

  async validate(payload: JwtPayload): Promise<User> {
    ....
    
  }
}