# 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 (opens new window) and Express-Session (opens new window) modules.

Note

The version of the keycloak-connect module should be the same version as your Keycloak instance.

npm install --save keycloak-connect
npm install --save express-session
npm install --save-dev @types/express-session
1
2
3

# 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 (opens new window).

# 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/common";
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;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 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/common";
import cors from "cors";
import compress from "compress";
import cookieParser from "cookie-parser";
import methodOverride from "method-override";
import session from "express-session";

@Configuration({
  middlewares: [
    cors(),
    compress({}),
    cookieParser(),
    methodOverride()
  ]
})
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());
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 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 {Context, MiddlewareMethods, Inject, Middleware} from "@tsed/common";
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);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# KeycloakAuthDecorator

To protect certain routes create a KeycloakAuthDecorator at src/decorators.

import {Returns} from "@tsed/schema";
import {UseAuth} from "@tsed/common";
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));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Protecting routes role-based in a controller

Now we can protect routes with our custom KeycloakAuth decorator.

import {Controller, Get} from "@tsed/common";
import {KeycloakAuth} from "../decorators/KeycloakAuthDecorator";

@Controller("/hello-world")
export class HelloWorldController {
  @Get("/")
  @KeycloakAuth({role: "realm:example-role"})
  get() {
    return "hello";
  }
}
1
2
3
4
5
6
7
8
9
10
11

# 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"}
              }
            }
          }
        }
      }
    }
  }
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Author

    Last Updated: 9/21/2022, 1:33:23 PM

    Other topics