ferrerojosh / nest-keycloak-connect

keycloak-nodejs-connect module for Nest
MIT License
316 stars 123 forks source link

Check user role manually #134

Closed AlessandroStaffolani closed 2 weeks ago

AlessandroStaffolani commented 2 years ago

I'm trying to build a custom guard to verify that the logger user is the same asking for resources belonging to him. For example, if I have a resource /users/alice/cats I want to verify that alice is the current user. In addition, my custom guard should allow access to the resource if the user has role admin.

For the first step I simply use the user in the request and verify the param, here is the code:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class UserOwnedGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const userId = request.params.userId;
    return user.sid === userId;
  }
}

However, to verify the second step I need to get the token object from the grant and call the hasRole method. How can I inject the keycloak instance on my guard?

ferrerojosh commented 2 years ago

If you want the keycloak instance, inject it using the KEYCLOAK_INSTANCE provider

AlessandroStaffolani commented 2 years ago

Can you provide an example of how to properly inject the instance? I tried in the app.module (where I register the KeycloakConnectModule) to export the provider, but when I'm injecting the instance nest cannot resolve it.

I inject the instance like so:

import {
  Injectable,
  CanActivate,
  ExecutionContext,
  Inject,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { KEYCLOAK_INSTANCE } from 'nest-keycloak-connect';
import { Keycloak } from 'keycloak-connect';

@Injectable()
export class UserOwnedGuard implements CanActivate {
  constructor(
    @Inject(KEYCLOAK_INSTANCE) private readonly keycloakInstance: Keycloak,
  ) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const userId = request.params.userId;
    return user.sid === userId;
  }
}
ferrerojosh commented 2 years ago

Can you provide the error ? Also is KeycloakConnectModule available in your root module ? Also keycloak instance type should be KeycloakConnect:

import KeycloakConnect from 'keycloak-connect';
AlessandroStaffolani commented 2 years ago

Here are all the details:

My app.module.ts:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import configuration from './config/configuration';
import {
  AuthGuard,
  KeycloakConnectModule,
  ResourceGuard,
  RoleGuard,
} from 'nest-keycloak-connect';
import { KeycloakConfigService } from './config/keycloak-config/keycloak-config.service';
import { KeycloakConfigModule } from './config/keycloak-config/keycloak-config.module';
import { APP_GUARD } from '@nestjs/core';
import { TemplatesModule } from "./resources/templates/templates.module";

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [configuration],
    }),
    KeycloakConnectModule.registerAsync({
      useExisting: KeycloakConfigService,
      imports: [KeycloakConfigModule],
    }),
    TemplatesModule
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
    {
      provide: APP_GUARD,
      useClass: ResourceGuard,
    },
    {
      provide: APP_GUARD,
      useClass: RoleGuard,
    },
  ],
})
export class AppModule {}

I have also changed the guard as suggested by you, but if I set as a type import KeycloakConnect from 'keycloak-connect'; for the injected instance typescript complains like so: Cannot use namespace 'KeycloakConnect' as a type.

This is the updated guard:

import {
  Injectable,
  CanActivate,
  ExecutionContext,
  Inject,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { KEYCLOAK_INSTANCE } from 'nest-keycloak-connect';
import KeycloakConnect from 'keycloak-connect';

@Injectable()
export class UserOwnedGuard implements CanActivate {
  constructor(
    @Inject(KEYCLOAK_INSTANCE)
    private readonly KeycloakConnect: KeycloakConnect,
  ) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const userId = request.params.userId;
    return user.sub === userId;
  }
}

Finally, if I remove the type and I run it the error I get is this:

Nest can't resolve dependencies of the UserOwnedGuard (?). Please make sure that the argument KEYCLOAK_INSTANCE at index [0] is available in the TemplatesModule context.

Potential solutions:
- If KEYCLOAK_INSTANCE is a provider, is it part of the current TemplatesModule?
- If KEYCLOAK_INSTANCE is exported from a separate @Module, is that module imported within TemplatesModule?
  @Module({
    imports: [ /* the Module containing KEYCLOAK_INSTANCE */ ]
  })

Where TemplatesModule is the module in which I'm using the guard. In the templates-controller I use the guard like so:

import { Controller, UseGuards } from '@nestjs/common';
import { TemplatesService } from './templates.service';
import { Roles } from 'nest-keycloak-connect';
import { UserOwnedGuard } from '../../lib/userOwnedGuard';
import { KeycloakRoles } from '../../config/keycloak-config/keycloak-roles';

@Roles({ roles: [KeycloakRoles.RealmUser] })
@UseGuards(UserOwnedGuard)
@Controller('users/:userId/templates')
export class TemplatesController {
  constructor(private readonly templatesService: TemplatesService) {}
}
AlessandroStaffolani commented 2 years ago

Any updates on this?

ferrerojosh commented 2 weeks ago

This can now be done with the implementation of @ConditionalScopes.