Skip to content

Instantly share code, notes, and snippets.

@paztek
Last active August 23, 2020 10:17
Show Gist options
  • Save paztek/5249e9cb7151fd0b9abf12546e6aa053 to your computer and use it in GitHub Desktop.
Save paztek/5249e9cb7151fd0b9abf12546e6aa053 to your computer and use it in GitHub Desktop.
nestjs-authentication-example-2
import { HttpModule, Module } from '@nestjs/common';
import { AuthenticationGuard } from './authentication.guard';
import { AuthenticationService } from './authentication.service';
import { AUTHENTICATION_STRATEGY_TOKEN } from './authentication.strategy';
import { KeycloakAuthenticationStrategy } from './strategy/keycloak.strategy';
import { FakeAuthenticationStrategy } from './strategy/fake.strategy';
@Module({
imports: [
HttpModule,
],
providers: [
AuthenticationGuard,
AuthenticationService,
{
provide: AUTHENTICATION_STRATEGY_TOKEN,
useClass: process.env.NODE_ENV === 'test' ? FakeAuthenticationStrategy : KeycloakAuthenticationStrategy,
},
],
exports: [
AuthenticationService,
],
})
export class AuthenticationModule {}
import { Inject, Injectable, Logger } from '@nestjs/common';
import { User } from './user.model';
import { AUTHENTICATION_STRATEGY_TOKEN, AuthenticationStrategy } from './authentication.strategy';
export class AuthenticationError extends Error {}
@Injectable()
export class AuthenticationService {
private logger = new Logger(AuthenticationService.name);
constructor(
@Inject(AUTHENTICATION_STRATEGY_TOKEN) private readonly strategy: AuthenticationStrategy,
) {}
async authenticate(accessToken: string): Promise<User> {
try {
const userInfos = await this.strategy.authenticate(accessToken);
const user = {
id: userInfos.sub,
username: userInfos.preferred_username,
};
/**
* Perform any addition business logic with the user:
* - insert user in "users" table on first authentication,
* - etc.
*/
return user;
} catch (e) {
this.logger.error(e.message, e.stackTrace);
throw new AuthenticationError(e.message);
}
}
}
export const AUTHENTICATION_STRATEGY_TOKEN = 'AuthenticationStrategy';
export interface KeycloakUserInfoResponse {
sub: string;
email_verified: boolean;
name: string;
preferred_username: string;
given_name: string;
family_name: string,
email: string;
}
export interface AuthenticationStrategy {
authenticate(accessToken: string): Promise<KeycloakUserInfoResponse>;
}
import * as jwt from 'jsonwebtoken';
import { v4 as uuid } from 'uuid';
import { KeycloakUserInfoResponse } from '../../../src/authentication/authentication.strategy';
export function createToken(id: string = uuid(), username = 'john.doe'): string {
const userInfos: KeycloakUserInfoResponse = {
sub: id,
email: `${username}@example.com`,
email_verified: true,
name: 'John doe',
preferred_username: username,
given_name: 'John',
family_name: 'Doe',
};
return jwt.sign(userInfos, 'secret');
}
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from './app.module';
import { INestApplication } from '@nestjs/common';
import { createToken } from '../test/helpers/authentication/create-token';
describe('Hello E2E', () => {
let app: INestApplication;
let server: any;
const token = createToken();
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [
AppModule
],
}).compile();
app = module.createNestApplication();
await app.init();
server = app.getHttpServer();
});
describe('GET /', () => {
it('requires authentication', async () => {
const response = await request(server)
.get('/');
expect(response.status).toEqual(401);
});
it('returns "Hello World!"', async () => {
const response = await request(server)
.get('/')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toEqual(200);
});
});
});
import { Injectable } from '@nestjs/common';
import * as jwt from 'jsonwebtoken';
import { AuthenticationStrategy, KeycloakUserInfoResponse } from '../authentication.strategy';
@Injectable()
export class FakeAuthenticationStrategy implements AuthenticationStrategy {
/**
* Blindly trust the JWT, assume it has the Keycloak structure and return the decoded payload
*/
public authenticate(accessToken: string): Promise<KeycloakUserInfoResponse> {
return Promise.resolve(jwt.decode(accessToken));
}
}
import { HttpService, Injectable } from '@nestjs/common';
import { AuthenticationStrategy, KeycloakUserInfoResponse } from '../authentication.strategy';
@Injectable()
export class KeycloakAuthenticationStrategy implements AuthenticationStrategy {
private readonly baseURL: string;
private readonly realm: string;
constructor(
private readonly httpService: HttpService,
) {
this.baseURL = process.env.KEYCLOAK_BASE_URL;
this.realm = process.env.KEYCLOAK_REALM;
}
/**
* Call the OpenId Connect UserInfo endpoint on Keycloak: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
*
* If it succeeds, the token is valid and we get the user infos in the response
* If it fails, the token is invalid or expired
*/
async authenticate(accessToken: string): Promise<KeycloakUserInfoResponse> {
const url = `${this.baseURL}/realms/${this.realm}/protocol/openid-connect/userinfo`;
const response = await this.httpService.get<KeycloakUserInfoResponse>(url, {
headers: {
authorization: `Bearer ${accessToken}`,
},
}).toPromise();
return response.data;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment