Skip to content

Instantly share code, notes, and snippets.

@ferrerojosh
Last active July 10, 2023 20:03
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ferrerojosh/653500646a0debf01548d315fe61a773 to your computer and use it in GitHub Desktop.
Save ferrerojosh/653500646a0debf01548d315fe61a773 to your computer and use it in GitHub Desktop.
Keycloak v9.0.0 NestJS Resource Guard
import { FactoryProvider, Logger } from '@nestjs/common';
import Keycloak from 'keycloak-connect';
export const KEYCLOAK_INSTANCE = 'KEYCLOAK_INSTANCE';
export const keycloakProvider: FactoryProvider = {
provide: KEYCLOAK_INSTANCE,
useFactory: () => {
const keycloakConfig: any = {
realm: '',
clientId: '',
secret: '',
authServerUrl: '',
};
const keycloak: any = new Keycloak({}, keycloakConfig);
// We just need it to return next(false) so our guard will be able to latch on
keycloak.accessDenied = (req, res, next) => {
next(false);
};
return keycloak;
}
};
import { SetMetadata } from '@nestjs/common';
export const Permissions = (...permissions: string[]) =>
SetMetadata('permissions', permissions);
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
Logger,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import KeycloakConnect from 'keycloak-connect';
import { KEYCLOAK_INSTANCE } from './keycloak.provider';
@Injectable()
export class ResourceGuard implements CanActivate {
constructor(
@Inject(KEYCLOAK_INSTANCE)
private keycloak: KeycloakConnect.Keycloak,
private readonly reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const permissions = this.reflector.get<string[]>(
'permissions',
context.getHandler(),
);
const [request, response] = [
this.getRequest(context),
context.switchToHttp().getResponse(),
];
const enforcerFn = createEnforcerContext(request, response);
return await enforcerFn(this.keycloak, permissions);
}
getRequest<T = any>(context: ExecutionContext): T {
return context.switchToHttp().getRequest();
}
}
const createEnforcerContext = (
request,
response,
) => (keycloak: KeycloakConnect.Keycloak, permissions: string[]) =>
new Promise<boolean>((resolve, reject) =>
keycloak.enforcer(permissions)(request, response, next => {
if (next !== undefined) {
resolve(false);
} else {
resolve(true);
}
}),
);
@rafaelbatistamarcilio
Copy link

That config works without provide a session store? I'm using keycloak-connect on a nest project too.

@ferrerojosh
Copy link
Author

That config works without provide a session store? I'm using keycloak-connect on a nest project too.

It works fine if you're only using it for REST APIs. You don't necessarily need session-store if you're not serving html pages.

@rafaelbatistamarcilio
Copy link

It works fine if you're only using it for REST APIs. You don't necessarily need session-store if you're not serving html pages.

This is exactly my case. I have an Angular client as static content of my Nest application.

I saw something about using the passport-js strategy with Nest and Keycloak PKCE, but I can't find a tutorial using Nest.

This library is referenced by keycloak docs, but I had no success in trying to implement it with Nest:https://github.com/exlinc/keycloak-passport

I'm considering to use an HTTP server such as NgInx and proxy all request with /api to the Nest app, then i will separate backend and frontend on 2 diffrent apps. Probaly on that way your strategy will work on my project.

Thanks.

@ferrerojosh
Copy link
Author

ferrerojosh commented Feb 19, 2020

This is exactly my case. I have an Angular client as static content of my Nest application.

I saw something about using the passport-js strategy with Nest and Keycloak PKCE, but I can't find a tutorial using Nest.

This library is referenced by keycloak docs, but I had no success in trying to implement it with Nest:https://github.com/exlinc/keycloak-passport

I'm considering to use an HTTP server such as NgInx and proxy all request with /api to the Nest app, then i will separate backend and frontend on 2 diffrent apps. Probaly on that way your strategy will work on my project.

Thanks.

Passport is an authentication middleware, this gist if for keycloak authorization, they are two different things.

If you are using Keycloak and want to use the @nestjs/passport authentication guard, you can use the following package: https://www.npmjs.com/package/passport-keycloak-bearer

And implement the passport strategy:

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import KeycloakBearerStrategy from 'passport-keycloak-bearer';

@Injectable()
export class KeycloakStrategy extends PassportStrategy(KeycloakBearerStrategy) {
  constructor() {
    super({
      realm: '',
      url: '',
    });
  }

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

If you want to use the library you referenced, you could also do that:

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import KeycloakStrategy from '@exlinc/keycloak-passport';

@Injectable()
export class KeycloakStrategy extends PassportStrategy(KeycloakStrategy) {
  constructor() {
    super({
      host: process.env.KEYCLOAK_HOST,
      realm: process.env.KEYCLOAK_REALM,
      clientID: process.env.KEYCLOAK_CLIENT_ID,
      clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
      callbackURL: `/api${AUTH_KEYCLOAK_CALLBACK}`
    });
  }

  async validate(accessToken, refreshToken, profile): Promise<any> {
    return profile;
  }
}

Then just call AuthGuard('keycloak') in Nest and you're good to go.

P.S I use passport-keycloak-bearer for authentication in my Nest app but not for keycloak resource authorization which this gist is about.

I saw something about using the passport-js strategy with Nest and Keycloak PKCE, but I can't find a tutorial using Nest.

You should be using keycloak-js in your Angular application (I use it too) if you want PKCE flow.

Also the Angular and Nest app should be two different clients in Keycloak, they can't be the same.

@SalahAdDin
Copy link

@ferrerojosh did you make it work?

@ferrerojosh
Copy link
Author

@ferrerojosh did you make it work?

I made a library for it.

@SalahAdDin
Copy link

@ferrerojosh does it work with GraphQL?

@ferrerojosh
Copy link
Author

@ferrerojosh does it work with GraphQL?

It should, some guy made a PR for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment