Skip to content

Commit

Permalink
Added a POC for OpenID via BFF
Browse files Browse the repository at this point in the history
  • Loading branch information
kristoff-kiefer committed Oct 6, 2023
1 parent 8d404cc commit cef470b
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 64 deletions.
229 changes: 183 additions & 46 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,21 @@
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.2",
"@nestjs/core": "^9.0.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "^7.0.4",
"@nestjs/terminus": "^9.0.0",
"@s3pweb/keycloak-admin-client-cjs": "^22.0.1",
"axios": "^1.5.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"express-session": "^1.17.3",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"nest-commander": "^3.11.0",
"nest-keycloak-connect": "^1.9.2",
"openid-client": "^5.6.0",
"passport": "^0.6.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0"
},
Expand All @@ -63,9 +67,11 @@
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.4.2",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.8",
"@types/jest": "^29.2.4",
"@types/lodash-es": "^4.17.9",
"@types/node": "^20.3.1",
"@types/passport": "^1.0.13",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.0.0",
Expand Down
23 changes: 15 additions & 8 deletions src/frontend/main.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* eslint-disable no-console */
import { INestApplication } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';

import { FrontendConfig, ServerConfig } from '../shared/config/index.js';
import { GlobalValidationPipe } from '../shared/validation/index.js';
import { FrontendModule } from './frontend.module.js';
import {INestApplication} from '@nestjs/common';

Check warning on line 2 in src/frontend/main.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Replace `INestApplication` with `·INestApplication·`
import {ConfigService} from '@nestjs/config';

Check warning on line 3 in src/frontend/main.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Replace `ConfigService` with `·ConfigService·`
import {NestFactory} from '@nestjs/core';

Check warning on line 4 in src/frontend/main.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Replace `NestFactory` with `·NestFactory·`
import {DocumentBuilder, OpenAPIObject, SwaggerModule} from '@nestjs/swagger';

Check warning on line 5 in src/frontend/main.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Replace `DocumentBuilder,·OpenAPIObject,·SwaggerModule` with `·DocumentBuilder,·OpenAPIObject,·SwaggerModule·`
import {FrontendConfig, ServerConfig} from '../shared/config/index.js';

Check warning on line 6 in src/frontend/main.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Replace `FrontendConfig,·ServerConfig` with `·FrontendConfig,·ServerConfig·`
import {GlobalValidationPipe} from '../shared/validation/index.js';

Check warning on line 7 in src/frontend/main.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Replace `GlobalValidationPipe` with `·GlobalValidationPipe·`
import {FrontendModule} from './frontend.module.js';

Check warning on line 8 in src/frontend/main.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Replace `FrontendModule` with `·FrontendModule·`
import session from 'express-session';

async function bootstrap(): Promise<void> {
const app: INestApplication = await NestFactory.create(FrontendModule);
Expand All @@ -20,6 +20,13 @@ async function bootstrap(): Promise<void> {
app.setGlobalPrefix('api', {
exclude: ['health'],
});
app.use(
session({
secret: 'my-secret',
resave: false,
saveUninitialized: false,
}),
);
SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, swagger));

const configService: ConfigService<ServerConfig, true> = app.get(ConfigService<ServerConfig, true>);
Expand Down
27 changes: 19 additions & 8 deletions src/modules/frontend/api/frontend.controller.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import { Controller, Post } from '@nestjs/common';
import { Controller, Get, Res, Session, UseGuards } from '@nestjs/common';
import { ApiAcceptedResponse, ApiTags } from '@nestjs/swagger';
import { AuthenticatedUser, Public, Resource } from 'nest-keycloak-connect';
import { Response } from 'express';

Check failure on line 3 in src/modules/frontend/api/frontend.controller.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

'express' should be listed in the project's dependencies. Run 'npm i -S express' to add it
import { LoginGuard } from '../login.guard.js';

@ApiTags('frontend')
@Controller({ path: 'frontend' })
export class FrontendController {
// Endpoints decorated with @Public are accessible to everyone
@Public()
@Resource('test')
@Post('login')
@UseGuards(LoginGuard)
@Get('login')
@ApiAcceptedResponse({ description: 'The person was successfully logged in.' })
public login(): string {
return 'Login!';
}

// Endpoints without @Public decorator automatically verify user
@Post('logout')
@Get('logout')
@ApiAcceptedResponse({ description: 'The person was successfully logged out.' })
public logout(@AuthenticatedUser() user: unknown): string {
public logout(): string {
// Can get logged in user with @AuthenticatedUser (technically any-type, is the JSON response from keycloak)
return `Logout! ${JSON.stringify(user)}`;
return 'Logout!';
}

@Get('callback')
@UseGuards(LoginGuard)
public callback(@Res() response: Response): void {
response.redirect('/api/frontend/loginInfo');
}

@Get('loginInfo')
public loginInfo(@Session() session: { passport: object }): string {
return JSON.stringify(session.passport)

Check warning on line 33 in src/modules/frontend/api/frontend.controller.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Insert `;`
}
}
23 changes: 21 additions & 2 deletions src/modules/frontend/frontend-api.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import { Module } from '@nestjs/common';
import { FrontendController } from './api/frontend.controller.js';
import { OpenidStrategy } from './openid.strategy.js';
import { Issuer } from 'openid-client';
import { PassportModule } from '@nestjs/passport';
import {SessionSerializer} from "./session.serializer.js";

Check warning on line 6 in src/modules/frontend/frontend-api.module.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Replace `SessionSerializer}·from·"./session.serializer.js"` with `·SessionSerializer·}·from·'./session.serializer.js'`

@Module({
imports: [],
providers: [],
imports: [PassportModule.register({ session: true, defaultStrategy: 'openid' })],
providers: [
{
provide: OpenidStrategy,
useFactory: async (): Promise<OpenidStrategy> => {
const TrustIssuer = await Issuer.discover(

Check warning on line 14 in src/modules/frontend/frontend-api.module.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Expected TrustIssuer to have a type annotation
'https://keycloak.dev.spsh.dbildungsplattform.de/realms/master',
);
const client = new TrustIssuer.Client({
client_id: 'spsh',
client_secret: 'YDp6fYkbUcj4ZkyAOnbAHGQ9O72htc5M',
});
return new OpenidStrategy(client);
},
},
SessionSerializer
],
controllers: [FrontendController],
})
export class FrontendApiModule {}
11 changes: 11 additions & 0 deletions src/modules/frontend/login.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LoginGuard extends AuthGuard('openid') {
public override async canActivate(context: ExecutionContext): Promise<boolean> {
const result = (await super.canActivate(context)) as boolean;
await super.logIn(context.switchToHttp().getRequest());
return result;
}
}
32 changes: 32 additions & 0 deletions src/modules/frontend/openid.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { PassportStrategy } from '@nestjs/passport';
import { AuthorizationParameters, Client, Strategy, StrategyOptions, TokenSet, UserinfoResponse } from 'openid-client';
import { UnauthorizedException } from '@nestjs/common';

export class OpenidStrategy extends PassportStrategy(Strategy, 'openid') {
public constructor(private client: Client) {
super({
client: client,
usePKCE: true,
params: { redirect_uri: 'http://127.0.0.1:9091/api/frontend/callback' },
} as StrategyOptions);
}

public async validate(tokenset: TokenSet): Promise<AuthorizationParameters> {
const userinfo: UserinfoResponse = await this.client.userinfo(tokenset);

try {
const idToken = tokenset.id_token;
const accessToken = tokenset.access_token;
const refreshToken = tokenset.refresh_token;
const user = {
id_token: idToken,
access_token: accessToken,
refresh_token: refreshToken,
userinfo,
};
return user;
} catch (err) {
throw new UnauthorizedException();
}
}
}
13 changes: 13 additions & 0 deletions src/modules/frontend/session.serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PassportSerializer } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class SessionSerializer extends PassportSerializer {
public serializeUser(user: unknown, done: (err: Error | null, user: unknown) => void): void {
done(null, user);
}

public deserializeUser(payload: string, done: (err: Error | null, payload: string) => void): void {
done(null, payload);
}
}

0 comments on commit cef470b

Please sign in to comment.