I’m trying to properly activate JwtStategy in a NestJs/Angular project of mine. The problem is that no matter what I do, stategy’s validate() function is not called. Users should be validated in a Microsoft Active Directory server.
Here’s my code:
NestJs Backend:
Authentication Module:
import { Module } from '@nestjs/common';
import { AuthenticationService } from './authentication.service';
import { ConfigService } from '@nestjs/config';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { MyProjectStrategy } from './genius.strategy';
import { AuthenticationController } from './authentication.controller';
import { UsersService } from 'src/users/users.service';
const jwtFactory = {
useFactory: async (configService: ConfigService) => {
return {
privateKey: configService.get<string>('JWT_PRIVATE_KEY_EC512', ''),
publicKey: configService.get<string>('JWT_PUBLIC_KEY_EC512', ''),
signOptions: {
expiresIn: configService.get('JWT_EXP_H'),
},
};
},
inject: [ConfigService],
};
@Module({
imports: [
JwtModule.registerAsync(jwtFactory),
PassportModule
],
controllers: [AuthenticationController],
providers: [
MyProjectStrategy,
ConfigService,
AuthenticationService,
UsersService
],
exports: [
AuthenticationService,
JwtModule,
MyProjectStrategy,
PassportModule
],
})
export class AuthenticationModule {}
Authenticaton Controller:
import { Controller, Get, Post, Param, Body, UseGuards } from '@nestjs/common';
import { AuthenticationService } from './authentication.service';
@Controller('authentication')
export class AuthenticationController {
constructor(
private readonly authenticationService: AuthenticationService
) { }
@Post('/authenticate/:username')
async postAutenticar(@Param('username') username: string, @Body() body: any): Promise<any> {
if (!body.password)
return { status: 500, msg: "Can't login - password or organization not informed", error: {}};
return await this.authenticationService.authenticate(username, body.password);
}
}
Autentication Service:
import { Injectable } from '@nestjs/common';
import 'dotenv/config';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { User } from '../common/models/user.model';
import { UsersService } from 'src/users/users.service';
export class AuthenticationError extends Error { }
@Injectable()
export class AuthenticationService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private configService: ConfigService
) {
}
async authenticate(username: string, password: string): Promise<{ accessToken: string}> {
let loggedUser: boolean | User = (await this.usersService.logUser(username, password)).result;
if (!(loggedUser instanceof User))
throw new AuthenticationError("Invalid user");
const accessToken: string = this.jwtService.sign(loggedUser.toPlainObj(), {
secret: this.configService.get('JWT_PRIVATE_KEY_EC512', ''),
algorithm: 'ES512',
expiresIn: 10000
});
if (loggedUser instanceof User)
console.log('User ', loggedUser.name, ' logged.');
return { accessToken };
}
async refreshToken(
username: string, password: string,
): Promise<{ accessToken: string }> {
let loggedUser = this.authenticate(username, password);
if (loggedUser instanceof User)
console.log('User ', loggedUser.name, ' updated token.');
return loggedUser;
};
}
Stategy:
import { Injectable, UnauthorizedException, Request } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { User } from 'src/common/models/user.model';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class MyProjectStrategy extends PassportStrategy(Strategy, 'myproject-jwt') {
constructor(
private usersService: UsersService,
private configService: ConfigService
) {
super({
secretOrKey: configService.get<string>('JWT_PUBLIC_KEY_EC512', ''),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
algorithms: ['ES512'],
});
}
async validate(user: any): Promise<boolean> {
console.log('validate');
return !!(await this.usersService.logUser(user.username, user.password)).result;
}
}
Guard:
import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport'
import { JwtService } from '@nestjs/jwt';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthenticationService } from './authentication.service';
import { Request } from 'express';
import 'dotenv/config';
import { User } from 'src/common/models/user.model';
import { TokenExpiredException } from './token-expired.exception';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class MyProjectGuard extends AuthGuard('myproject-jwt') {
constructor(
private readonly authenticationService: AuthenticationService,
private jwtService: JwtService,
private configService: ConfigService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('JWT'),
secretOrKey: configService.get<string>('JWT_PUBLIC_KEY_EC512', ''),
});
}
async canActivate(context: ExecutionContext): Promise<boolean> {
console.log('canActivate');
const request: Request = context.switchToHttp().getRequest();
const token = request.header('Authorization');
if (!token)
throw new HttpException('Header Bearer <token> missing', HttpStatus.UNAUTHORIZED);
const parts = token.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer')
throw new HttpException('Header Bearer <token> invalid', HttpStatus.UNAUTHORIZED);
let user = new User(this.jwtService.decode(parts[1]));
if (user.administrator)
return true;
else
throw new HttpException('Usuário não autorizado', HttpStatus.UNAUTHORIZED);
}
handleRequest(err, user, info) {
console.log('handleRequest')
if (err || !user) {
if (info ? info.message.indexOf('expired') >= 0 : false)
throw new TokenExpiredException();
else
throw err || new UnauthorizedException();
}
return user;
}
}
Users controller:
import { Controller, Get, Post, Param, Body, UseGuards } from '@nestjs/common';
import { UsersService } from './users.service';
import { MyProjectGuard } from '../authentication/genius.guard';
@Controller('users')
export class UsersController {
constructor(
private readonly usersService: UsersService
) { }
@UseGuards(MyProjectGuard)
@Get('/read/:username')
async getObter(@Param('username') username: string): Promise<any> {
console.log('OK');
return this.usersService.readUser(username);
}
@Post('/log/:username')
async postLogar(@Param('username') username: string, @Body() body: any): Promise<any> {
if (!body.password)
return { status: 500, msg: "Can't login - password or organization not informed", error: {}};
return await this.usersService.logUser(username, body.password);
}
}
Users service:
import { Injectable } from '@nestjs/common';
import { Client, Attribute, Change } from 'ldapts';
import fs from 'fs';
import 'dotenv/config';
import { User } from 'src/common/models/user.model';
import { BeResult } from 'src/common/models/beResult.model';
@Injectable()
export class UsersService {
client: Client;
searchDN: string;
baseDN: string;
bindDN: string = process.env.LDAP_BIND_DN;
admPwd: string = process.env.LDAP_BIND_PWD;
constructor() {
this.searchDN = process.env.LDAP_USERS_BASE_DN;
this.baseDN = process.env.LDAP_USERS_BASE_DN;
let certCA = fs.readFileSync(process.env.LDAP_CERT_CA_PATH);
this.client = new Client({
url: process.env.LDAP_URL,
timeout: 3000,
connectTimeout: 5000,
tlsOptions: {
ca: [certCA],
}
});
this.client.bind(this.bindDN, this.admPwd);
}
(...)
async _readUser(username) {
const { searchEntries } = await this.client.search(this.searchDN, {
scope: 'sub',
filter: `&(objectClass=person)(sAMAccountName=${this.susername(username)})`,
attributes: ['objectClass', 'sn', 'name', 'description', 'sAMAccountName', 'mail', 'telephoneNumber', 'street', 'title', 'unicodePwd'],
});
let result = searchEntries.length ? this.convertUserLdapToUser(searchEntries[0]) : {};
return result;
}
async readUser(username: string) {
return { status: 200, msg: 'OK', result: await this._readUser(username) }
}
async logUser(username: string, password: string) : Promise<BeResult> {
let res: boolean = false;
let error: any;
let user: User | {};
try {
await this.client.unbind();
await this.client.bind(`mydomain\${this.susername(username)}`, password);
res = true;
} catch(e: any) {
if (e.name != 'InvalidCredentialsError')
error = e;
} finally {
if (res) {
user = await this._readUser(username);
}
await this.client.unbind();
await this.client.bind(this.bindDN, this.admPwd);
return erro ? { status: 500, msg: "Can't log", erro } :
{ status: 200, msg: res ? "Logged user" : "Invalid Credential", result: res ? user : false };
}
}
(...)
}
Frontend:
Users service:
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
import { User } from '../model/user.model';
import { BehaviorSubject, catchError, map, Observable, tap } from 'rxjs';
import { BeResult } from '../model/beResult.model';
import { HttpClient } from '@angular/common/http';
import { ProcessHTTPMsgService } from './process-httpmsg.service';
import { jwtDecode } from "jwt-decode";
@Injectable({
providedIn: 'root'
})
export class UsersService {
token: string = '';
constructor(
private http: HttpClient,
private processHTTPMsgService: ProcessHTTPMsgService,
) {
}
private currentUserSource = new BehaviorSubject<User>(new User({.name: 'My Name (test)', organizacao: 'My Organization (test)' }));
currentUser = this.currentUserSource.asObservable();
getHeaders() {
return { headers: { 'Authorization': `Bearer ${this.token}` } };
}
setCurrentUser(user: User) {
this.currentUserSource.next(user);
}
(...)
log(user: User): Observable<boolean | User | {}> {
let body: any = user;
body['headers'] = this.getHeaders();
return this.http
.post<BeResult>(`${environment.apiUrl}/authentication/authenticate/${user.username}`, body)
.pipe(map(r => this.convertResultado(r)), catchError(this.processHTTPMsgService.handleError));
}
public getUserByUsername(user: User): Observable<User> {
return this.http
.get<BeResult>(`${environment.apiUrl}/users/read/${user.username}`, this.getHeaders())
.pipe(tap(console.log), map(r => r.result), catchError(this.processHTTPMsgService.handleError));
}
}
Login component:
import { Component } from '@angular/core';
import { InputTextModule } from 'primeng/inputtext';
import { CardModule } from 'primeng/card';
import { ButtonModule } from 'primeng/button';
import { Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { UsersService } from '../../common/services/users.service';
import { User } from '../../common/model/user.model';
@Component({
selector: 'app-login',
standalone: true,
imports: [InputTextModule, CardModule, ButtonModule, RouterLink, FormsModule],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
export class LoginComponent {
constructor(
private usersService: UsersService,
private router: Router
) { }
username: string = '';
password: string = '';
log() {
let user = new User();
user.username = this.username;
user.password = this.password;
this.usersService.log(user).subscribe((user: boolean | User | {}) => {
if (user instanceof User && user.administrator) {
this.usersService.setCurrentUser(user);
this.router.navigate(['one-user']);
}
})
}
}
Component that tried to access a protected route:
import { Component } from '@angular/core';
(...)
import { UsersService } from '../../common/services/users.service';
import { User } from '../../common/model/user.model';
@Component({
selector: 'app-one-user',
standalone: true,
imports: [InputTextModule, InputMaskModule, PasswordModule, DividerModule, ButtonModule],
templateUrl: './one-user.component.html',
styleUrl: './one-user.component.scss'
})
export class UmUserComponent {
constructor(
private usersService: UsersService
) {
}
gravar() {
let user = new User({ username: 'myusername'});
this.usersService.getUserByUsername(user).subscribe((user: User) => {
console.log('user', user)
})
}
}
I’ve seen several questions regarding this subject and can’t see anyone that is doing nothing very different of what I’m doing. What’s the problem here ?
In one of the questions I saw, says that validate() is only called when the payload is correct. I’m reazonably certain the payload is correct as it apears correctly when I debug it.
Any ideas ?
I had to translate several parts of the code to english and revised it 3x. Please apologize me if I missed something.