Why isn't validate function on a NestJs strategy class not being called?

Discussão em 'Angular' iniciado por Nelson Teixeira, Novembro 30, 2024.

  1. 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.

    Continue reading...

Compartilhe esta Página