Keycloak
This tutorial shows you how you can secure your Ts.ED application with an existing Keycloak instance.
Installation
Before securing the application with Keycloak, we need to install the Keycloak Node.js Adapter and Express-Session modules.
Note
The version of the keycloak-connect
module should be the same version as your Keycloak instance.
npm install --save keycloak-connect express-session
npm install --save-dev @types/express-session
yarn add keycloak-connect express-session
yarn add -D @types/express-session
pnpm add keycloak-connect express-session
pnpm add -D @types/express-session
bun add keycloak-connect express-session
bun add -D @types/express-session
Download keycloak.json
Put the keycloak.json file for your Keycloak client to src/config/keycloak
.
How exactly the file is downloaded can be found in the official Keycloak documentation.
KeycloakService
Create a KeycloakService in src/services
that handles the memory store, the Keycloak instance and the token.
import {Service} from "@tsed/di";
import {MemoryStore} from "express-session";
import {$log} from "@tsed/logger";
import {Token} from "keycloak-connect";
import KeycloakConnect = require("keycloak-connect");
@Service()
export class KeycloakService {
private keycloak: KeycloakConnect.Keycloak;
private memoryStore: MemoryStore;
private token: Token;
constructor() {
this.initKeycloak();
}
public initKeycloak(): KeycloakConnect.Keycloak {
if (this.keycloak) {
$log.warn("Trying to init Keycloak again!");
return this.keycloak;
} else {
$log.info("Initializing Keycloak...");
this.memoryStore = new MemoryStore();
this.keycloak = new KeycloakConnect({store: this.memoryStore}, "src/config/keycloak/keycloak.json");
return this.keycloak;
}
}
public getKeycloakInstance(): KeycloakConnect.Keycloak {
return this.keycloak;
}
public getMemoryStore(): MemoryStore {
return this.memoryStore;
}
public getToken(): Token {
return this.token;
}
public setToken(token: Token): void {
this.token = token;
}
}
Add KeycloakService to Server
Make sure that the KeycloakService is part of the componentsScan array of the global configuration.
The KeycloakService
can then be injected in the Server class and the middleware of express-session
and keycloak-connect
can be called.
import {Configuration, Inject} from "@tsed/di";
import {PlatformApplication} from "@tsed/platform-http";
import session from "express-session";
@Configuration({
middlewares: ["cors", "compression", "cookie-parser", "method-override", "json-parser", "urlencoded-parser"]
})
export class Server {
@Inject()
protected app: PlatformApplication;
@Inject()
protected keycloakService: KeycloakService;
@Configuration()
protected settings: Configuration;
$beforeRoutesInit(): void {
this.app.use(
session({
secret: "thisShouldBeLongAndSecret",
resave: false,
saveUninitialized: true,
store: this.keycloakService.getMemoryStore()
})
);
this.app.use(this.keycloakService.getKeycloakInstance().middleware());
}
}
KeycloakMiddleware
To secure your routes add a KeycloakMiddleware class to src/middlewares
.
With each request the token is set to the request property kauth
.
In order to be able to use the token we set this in the KeycloakService.
import {MiddlewareMethods, Middleware} from "@tsed/platform-middlewares";
import {Inject} from "@tsed/di";
import {Context} from "@tsed/platform-params";
import {KeycloakAuthOptions} from "../decorators/KeycloakAuthDecorator";
import {KeycloakService} from "../services/KeycloakService";
@Middleware()
export class KeycloakMiddleware implements MiddlewareMethods {
@Inject()
protected keycloakService: KeycloakService;
public use(@Context() ctx: Context) {
const options: KeycloakAuthOptions = ctx.endpoint.store.get(KeycloakMiddleware);
const keycloak = this.keycloakService.getKeycloakInstance();
if (ctx.getRequest().kauth.grant) {
this.keycloakService.setToken(ctx.getRequest().kauth.grant.access_token);
}
return keycloak.protect(options.role);
}
}
KeycloakAuthDecorator
To protect certain routes create a KeycloakAuthDecorator at src/decorators
.
import {Returns} from "@tsed/schema";
import {UseAuth} from "@tsed/platform-middlewares";
import {useDecorators} from "@tsed/core";
import {Security} from "@tsed/schema";
import {KeycloakMiddleware} from "../middlewares/KeycloakMiddleware";
export interface KeycloakAuthOptions extends Record<string, any> {
role?: string;
scopes?: string[];
}
export function KeycloakAuth(options: KeycloakAuthOptions = {}): Function {
return useDecorators(UseAuth(KeycloakMiddleware, options), Security("oauth2", ...(options.scopes || [])), Returns(403));
}
Protecting routes role-based in a controller
Now we can protect routes with our custom KeycloakAuth decorator.
import {Get} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {KeycloakAuth} from "../decorators/KeycloakAuthDecorator";
@Controller("/hello-world")
export class HelloWorldController {
@Get("/")
@KeycloakAuth({role: "realm:example-role"})
get() {
return "hello";
}
}
Swagger integration
If you would like to log in directly from your Swagger UI add the following code to your Swagger config.
Don't forget to replace authorizationUrl
, tokenUrl
and refreshUrl
with your custom keycloak URLs.
swagger: [
{
path: `/v3/docs`,
specVersion: "3.0.1",
spec: {
components: {
securitySchemes: {
oauth2: {
type: "oauth2",
flows: {
authorizationCode: {
authorizationUrl: "https://<keycloak-url>/auth/realms/<my-realm>/protocol/openid-connect/auth",
tokenUrl: "https://<keycloak-url>/auth/realms/<my-realm>/protocol/openid-connect/token",
refreshUrl: "https://<keycloak-url>/auth/realms/<my-realm>/protocol/openid-connect/token",
scopes: {openid: "openid", profile: "profile"}
}
}
}
}
}
}
}
];