Skip to main content

Command Palette

Search for a command to run...

Understanding the Powerful Keycloak Tool

Published
9 min read
Understanding the Powerful Keycloak Tool

Authentication Server with NestJS

Index

  1. Tutorial
  2. Installation
  3. Setup
  4. Add a New Realm
  5. OpenID Connect
  6. Integration NestJS + KeyCloak
  7. ReactiveX
  8. Internal Router Docker
  9. Multi-Tenancy Implementation
  10. Setup MySQL Database with Docker
  11. Setup Sequelize ORM
  12. Create NestJS Module Mult Tenant
  13. H2 Database Setup
  14. Keycloak email Setup
  15. MySQL Integration
  16. Migration Database
  17. Keycloak Tests
  18. References

1. Tutorial

1.1 Start Environment

1.1.1 KeyCloak Environment

// Start Docker
sudo dockerd

docker images

// Start Docker Keyclak App
docker start keycloak_app_1

KeyCloak Admin Console

1.1.2 KeyCloak API Test

Demo Key Cloak API

1.1.3 Backend NestJS with KeyCloak

Start NestJS with Docker

docker-compose up

1.1.4 Database with support to Backend NestJS with KeyCloak

2. Installation KeyCloak

Install Keycloak

docker run -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:16.1.0

by Docker

  • docker-compose.yaml

version: "3"

services:

app:

image: jboss/keycloak:15.0.0

# volumes:

# - ./.docker/keycloak/data:/opt/jboss/keycloak/standalone/data

environment:

- KEYCLOAK_USER=admin

- KEYCLOAK_PASSWORD=admin

#- KEYCLOACK_IMPORT=/tmp/test-realm-export.json

- DB_VENDOR=h2

ports:

- 8080:8080

#Start Docker Main Deamon
sudo dockerd &

#Download & Up Docker Image
docker-compose up

# localhost:8080

docker start keycloak_app_1

Docker Initialize

sudo dockerd &

docker images

docker start keycloak_app_1

- Http management interface listening on http://127.0.0.1:9990/management

- Admin console listening on
http://127.0.0.1:9990

# Auth

http://127.0.0.1:8080/auth/

http://127.0.0.1:8080/management

3. Setup KeyCloak

KeyCloak

add a New Realm

OAuth2 Concepts

Resource Owner

  • Spotfy — Luiz

Client

  • Application Spotify

Resource Server

  • Facebook

Authorization Server

  • Facebook
  • access_token | login
  • Open ID Connect = OAuth2 + Login

SAML2

  • Web Application
  • auth server
  • XML

Open ID Connect

Template Engine

  • FreeMarker Java

[What is Apache FreeMarker™?
Apache FreeMarker™ is a template engine: a Java library to generate text output (HTML web pages, e-mails, configuration…freemarker.apache.org](https://freemarker.apache.org/ "https://freemarker.apache.org/")

Add KeyCloak Client

add clients nest (Backend)

Access Type

  • Select => confidential
  • Backend App => Confidential

  • Secret
  • 84c8ea8c-5e7e-48ea-a38f-61547e3d4ad7

Access Type:

  • Confidential
  • Public
  • Bear-Only

Connection KeyClak

POST http://localhost:8080/auth/realms/fullcycle/protocol/openid-connect/token

Content-Type: application/x-www-form-urlencoded

client_id=nest

&client_secret=84c8ea8c-5e7e-48ea-a38f-61547e3d4ad7

&grant_type=password

&username=user1@user.com

&password=123456

4. OpenID Connect

https://openid.net/

JWT Token

eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJZamg3TDNjbzhHNlU4Ykh0Rlg4Q3h1Y3phY0JEY0ZiMHJCRWhCSmRCTzQ4In0.eyJleHAiOjE2NDI1Mjk1NTUsImlhdCI6MTY0MjUyOTI1NSwianRpIjoiNDZkM2ZiN2QtNTkwMi00NGU5LWI1MWUtM2QxNWQ0YmYzYzgwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2Z1bGxjeWNsZSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI2ZjE4OTExNi1hNTQyLTQzNmItYmE1ZC00MDUyMmMxYTZhZTAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJuZXN0Iiwic2Vzc2lvbl9zdGF0ZSI6IjRiMWM3MTdmLTAyMWMtNDY0My05ZGY0LWZiOWM1YjMwZDEzNyIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDozMDAwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZ2VyZW50ZSIsImRlZmF1bHQtcm9sZXMtZnVsbGN5Y2xlIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiNGIxYzcxN2YtMDIxYy00NjQzLTlkZjQtZmI5YzViMzBkMTM3IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiVXNlcjEgTGFzdCBOYW1lMSIsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIxQHVzZXIuY29tIiwiZ2l2ZW5fbmFtZSI6IlVzZXIxIiwiZmFtaWx5X25hbWUiOiJMYXN0IE5hbWUxIiwiZW1haWwiOiJ1c2VyMUB1c2VyLmNvbSJ9.aDlJv-JUxxWtepMuiJsNgVP0EIxBZ6pybr9Nsqhn8u42Ai3t2R4JRkUzk97zHjOXEtkPc6yKJ_BCEwEjlHyAHba2Tv_3UzdT0geAguvnDRyIi7T3D1feYG8UMnEn-NxO1LVdb8XSq5OENsmZZZtYQavXY12nqv5c3Go_Gqr9QeA1ijr6NCZnKi01hGpbhkE85dt2wJK_XIFizUzsBRF-hW9nwMZrKPSBAGFXU8y0OY9UOZtE21mgeFmqFqR48yTvC9sBTeumm0RTFkyU_wws2gq6JCFtFASIvt3pwl-chKAzXPm1IKOUn14kQFo8Qhw49URh3jTMk97xQHEi82xWIg","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiNjBkYTAwOS05YWMyLTQ4MjgtOTkyYS05MzI4NDQxZTYzMGMifQ.eyJleHAiOjE2NDI1MzEwNTUsImlhdCI6MTY0MjUyOTI1NSwianRpIjoiODhmYWM0ZWMtNTg1ZC00YzE4LTk4MWYtYWEyZjMzZDNiNzNjIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2Z1bGxjeWNsZSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hdXRoL3JlYWxtcy9mdWxsY3ljbGUiLCJzdWIiOiI2ZjE4OTExNi1hNTQyLTQzNmItYmE1ZC00MDUyMmMxYTZhZTAiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibmVzdCIsInNlc3Npb25fc3RhdGUiOiI0YjFjNzE3Zi0wMjFjLTQ2NDMtOWRmNC1mYjljNWIzMGQxMzciLCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI0YjFjNzE3Zi0wMjFjLTQ2NDMtOWRmNC1mYjljNWIzMGQxMzcifQ.lx23mdtPF918kVEnmT78_yco2Wc4fSo5jOasB3MyPbI

https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJZamg3TDNjbzhHNlU4Ykh0Rlg4Q3h1Y3phY0JEY0ZiMHJCRWhCSmRCTzQ4In0.eyJleHAiOjE2NDI1Mjk1NTUsImlhdCI6MTY0MjUyOTI1NSwianRpIjoiNDZkM2ZiN2QtNTkwMi00NGU5LWI1MWUtM2QxNWQ0YmYzYzgwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2Z1bGxjeWNsZSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI2ZjE4OTExNi1hNTQyLTQzNmItYmE1ZC00MDUyMmMxYTZhZTAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJuZXN0Iiwic2Vzc2lvbl9zdGF0ZSI6IjRiMWM3MTdmLTAyMWMtNDY0My05ZGY0LWZiOWM1YjMwZDEzNyIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDozMDAwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZ2VyZW50ZSIsImRlZmF1bHQtcm9sZXMtZnVsbGN5Y2xlIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiNGIxYzcxN2YtMDIxYy00NjQzLTlkZjQtZmI5YzViMzBkMTM3IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiVXNlcjEgTGFzdCBOYW1lMSIsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIxQHVzZXIuY29tIiwiZ2l2ZW5fbmFtZSI6IlVzZXIxIiwiZmFtaWx5X25hbWUiOiJMYXN0IE5hbWUxIiwiZW1haWwiOiJ1c2VyMUB1c2VyLmNvbSJ9.aDlJv-JUxxWtepMuiJsNgVP0EIxBZ6pybr9Nsqhn8u42Ai3t2R4JRkUzk97zHjOXEtkPc6yKJ_BCEwEjlHyAHba2Tv_3UzdT0geAguvnDRyIi7T3D1feYG8UMnEn-NxO1LVdb8XSq5OENsmZZZtYQavXY12nqv5c3Go_Gqr9QeA1ijr6NCZnKi01hGpbhkE85dt2wJK_XIFizUzsBRF-hW9nwMZrKPSBAGFXU8y0OY9UOZtE21mgeFmqFqR48yTvC9sBTeumm0RTFkyU_wws2gq6JCFtFASIvt3pwl-chKAzXPm1IKOUn14kQFo8Qhw49URh3jTMk97xQHEi82xWIg%22%2C%22expires_in%22%3A300%2C%22refresh_expires_in%22%3A1800%2C%22refresh_token%22%3A%22eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiNjBkYTAwOS05YWMyLTQ4MjgtOTkyYS05MzI4NDQxZTYzMGMifQ.eyJleHAiOjE2NDI1MzEwNTUsImlhdCI6MTY0MjUyOTI1NSwianRpIjoiODhmYWM0ZWMtNTg1ZC00YzE4LTk4MWYtYWEyZjMzZDNiNzNjIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2Z1bGxjeWNsZSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hdXRoL3JlYWxtcy9mdWxsY3ljbGUiLCJzdWIiOiI2ZjE4OTExNi1hNTQyLTQzNmItYmE1ZC00MDUyMmMxYTZhZTAiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibmVzdCIsInNlc3Npb25fc3RhdGUiOiI0YjFjNzE3Zi0wMjFjLTQ2NDMtOWRmNC1mYjljNWIzMGQxMzciLCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI0YjFjNzE3Zi0wMjFjLTQ2NDMtOWRmNC1mYjljNWIzMGQxMzcifQ.lx23mdtPF918kVEnmT78_yco2Wc4fSo5jOasB3MyPbI&publicKey=%7B%0A%20%20%22e%22%3A%20%22AQAB%22%2C%0A%20%20%22kty%22%3A%20%22RSA%22%2C%0A%20%20%22n%22%3A%20%22jBojl_HQ8J0BXCtLTnX0hQBLfIflbPclukIFwrFQ2JY9wSACXpOhO2vC6NLu02JO2r9z68VnxTgov8LuCArL_zzr4XZsOATK8bKdT6GI_bcsoCH0yJ0_CJ5go6KIOraQbsGI7rjWW_2If-5xfucJ4apiX1XpDAgKEOLV9tTCwMc-G7zPMFEiVZbS9HPI7BHPkYkHUmpR2K6klP7qSW9PnpFnGz1J6_vkP6yDUKYVkg7cUIV93rVcZvAXNGrLOmgvVAouLFFGgRGnKj-wdUFtRofVeOjYTnFwcot9P2wADQz8IkpD15NmY_l2PgB3uigRi7I83oWAwWVuFhNuxoCYcQ%22%0A%7D

a) Header

Header JWT

b) Payload

Payload

Payload

{
"exp": 1642529555,
"iat": 1642529255,
"jti": "46d3fb7d-5902-44e9-b51e-3d15d4bf3c80",
"iss": "http://localhost:8080/auth/realms/fullcycle",
"aud": "account",
"sub": "6f189116-a542-436b-ba5d-40522c1a6ae0",
"typ": "Bearer",
"azp": "nest",
"session_state": "4b1c717f-021c-4643-9df4-fb9c5b30d137",
"acr": "1",
"allowed-origins": [
"http://localhost:3000"
],
"realm_access": {
"roles": [
"offline_access",
"uma_authorization",
"gerente",
"default-roles-fullcycle"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "email profile",
"sid": "4b1c717f-021c-4643-9df4-fb9c5b30d137",
"email_verified": false,
"name": "User1 Last Name1",
"preferred_username": "user1@user.com",
"given_name": "User1",
"family_name": "Last Name1",
"email": "user1@user.com"
}

c) Signature

Signature

Test Get Method

POST http://localhost:8080/auth/realms/fullcycle/protocol/openid-connect/token

Content-Type: application/x-www-form-urlencoded

client_id=nest

&client_secret=84c8ea8c-5e7e-48ea-a38f-61547e3d4ad7

&grant_type=password

&username=user1@user.com

&password=123456

###

GET http://localhost:8080/auth/realms/fullcycle/protocol/openid-connect/userinfo

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJZamg3TDNjbzhHNlU4Ykh0Rlg4Q3h1Y3phY0JEY0ZiMHJCRWhCSmRCTzQ4In0.eyJleHAiOjE2NDI1NzA4NjcsImlhdCI6MTY0MjUzNDg2NywianRpIjoiZjQzZjc5MjMtNWVhNS00MzI1LWE3OTEtOGQzOTVlNTFmNTI1IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2Z1bGxjeWNsZSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI2ZjE4OTExNi1hNTQyLTQzNmItYmE1ZC00MDUyMmMxYTZhZTAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJuZXN0Iiwic2Vzc2lvbl9zdGF0ZSI6IjM1ZDE5ODBmLTQ3YTktNGNlMS05YzU3LTU5ZGM0MDRlYzAxOCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDozMDAwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZ2VyZW50ZSIsImRlZmF1bHQtcm9sZXMtZnVsbGN5Y2xlIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiMzVkMTk4MGYtNDdhOS00Y2UxLTljNTctNTlkYzQwNGVjMDE4IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiVXNlcjEgTGFzdCBOYW1lMSIsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIxQHVzZXIuY29tIiwiZ2l2ZW5fbmFtZSI6IlVzZXIxIiwiZmFtaWx5X25hbWUiOiJMYXN0IE5hbWUxIiwiZW1haWwiOiJ1c2VyMUB1c2VyLmNvbSJ9.Rwlqx6WdQ4V4QwPYOYtiHnOxAtokssUjvcG2-q03CFvOTXfbuzvhzY6szndzAH2XFC2dY2MhGRZeWCiiTyGM5z_heSMf5atZ36rFM7ujEW__S-WIoDRAWqAmaPErnWB5YWtuhvUjRk9ToLbcm6P0R70fY0ItZ7CRva2SoTXEDoOrE9HChRVyP-9S-Ecesjn4-s0dMg5w_ANgsnAzw76GqWpfN-X6cbpB3IRN2fIXArP3CccpJIE708SHmNAQaUlPt6VM5VTdVB6isX8ujpWA8egyWqw2zAuqz-xyR2FrtVIndNhDAoKk3AinMyceLLImIF-5c9olHekXE8FYHwYnxA

Tokens JWT

  • Access Token
  • ID Token

5.Integration NestJS + KeyCloak

  • jwt-strategy.service.ts

import { Injectable } from '@nestjs/common';

import { PassportStrategy } from '@nestjs/passport';

import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()

export class JwtStrategyService extends PassportStrategy(Strategy, 'jwt') {

constructor() {

super({

jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),

ignoreExpiration: false,

secretOrKey: 'abcd123456',

});

}

async validate(payload) {

return payload;

}

}

  • docker-compose.yml

version: '3'

services:

app:

build: .

entrypoint: ./entrypoint.sh

ports:

- 3000:3000

volumes:

- .:/home/node/app

  • Docker Compose Up

docker-compose up

docker-compose exec app bash

npm install [@nestjs/axios](http://twitter.com/nestjs/axios "Twitter profile for @nestjs/axios") --save

6. ReactiveX

An API for asynchronous programming
with observable streams

[ReactiveX
Edit descriptionreactivex.io](https://reactivex.io/ "https://reactivex.io/")

import { firstValueFrom } from 'rxjs';

7. Internal Router Docker

  • auth.service.ts

import { HttpService } from '@nestjs/axios';

import { Injectable } from '@nestjs/common';

import { firstValueFrom } from 'rxjs';

//bcrypt

@Injectable()

export class AuthService {

constructor(private http:HttpService) {}

async login(username: string, password: string): Promise<void>{

const { data } = await firstValueFrom(

this.http.post(

'http://host.docker.internal/auth/realms/fullcycle/protocol/openid-connect/token',

new URLSearchParams({

client_id:'nest',

client_secret:'84c8ea8c-5e7e-48ea-a38f-61547e3d4ad7',

grant_type:'password',

username,

password,

// username:'user1@user.com',

// password:'123456'

})

));

return data;

}

}

  • docker-compose.yaml

version: '3'

services:

app:

build: .

entrypoint: ./entrypoint.sh

ports:

- 3000:3000

volumes:

- .:/home/node/app

extra_hosts:

- "host.docker.internal:172.17.0.1"

//etc/hosts

127.0.0.1 host.docker.internal

KeyCloak
Port: 8080

  • /etc/hosts
  • Windows 10
  • aa

C:\Windows\System32\drivers\etc\hosts

HTTP/1.1 201 Created

X-Powered-By: Express

Content-Type: application/json; charset=utf-8

Content-Length: 2370

ETag: W/"942-K7GG7R7oh3DcCQW7N3JdyLFcgkA"

Date: Wed, 19 Jan 2022 00:21:15 GMT

Connection: close

{

"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJZamg3TDNjbzhHNlU4Ykh0Rlg4Q3h1Y3phY0JEY0ZiMHJCRWhCSmRCTzQ4In0.eyJleHAiOjE2NDI1ODc2NzUsImlhdCI6MTY0MjU1MTY3NSwianRpIjoiZjBlMGY0MDMtNjc1MC00ZWJhLTk4NzEtZmRmMGQ4Y2QxNDgzIiwiaXNzIjoiaHR0cDovL2hvc3QuZG9ja2VyLmludGVybmFsOjgwODAvYXV0aC9yZWFsbXMvZnVsbGN5Y2xlIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjZmMTg5MTE2LWE1NDItNDM2Yi1iYTVkLTQwNTIyYzFhNmFlMCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im5lc3QiLCJzZXNzaW9uX3N0YXRlIjoiODI5MTJjZWEtMzI4ZC00Y2ZkLTg1NTQtZWU2Y2RmMWUyMzQ5IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjMwMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJnZXJlbnRlIiwiZGVmYXVsdC1yb2xlcy1mdWxsY3ljbGUiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI4MjkxMmNlYS0zMjhkLTRjZmQtODU1NC1lZTZjZGYxZTIzNDkiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJVc2VyMSBMYXN0IE5hbWUxIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlcjFAdXNlci5jb20iLCJnaXZlbl9uYW1lIjoiVXNlcjEiLCJmYW1pbHlfbmFtZSI6Ikxhc3QgTmFtZTEiLCJlbWFpbCI6InVzZXIxQHVzZXIuY29tIn0.UGLiygD0SgoBvS4-CkPI6duW-ROfvL2cUJ3KHJhxMYfV4n0PWBWQ00K_R8lJ2hKVx0_9zhLyPp-JWYiBdnZJIHyfsQmLMZHGggaKRP9X5HO9bOxAHf5flfE4OrZBFmQ8bnVV1ENm4gRtBMYlBr2HDgJbx6bshN2pXwZU8t0bS8_S6cnX82fkXnVoRWiU0uHG_UbK_JSaL8Eupo7DY1wUpo5d7WxsQ4pQKxcKLMo3hgLsrN_TjUnqQab6xpicGbrHzNIEw75IaRp0yenGymNEIh8rdZlOcPuWH6IRp8JKA8r27TOjZX2cnUN2g2g5ChoyhaSc5trOUGLbVzkpWEn8Qw",

"expires_in": 36000,

"refresh_expires_in": 1800,

"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiNjBkYTAwOS05YWMyLTQ4MjgtOTkyYS05MzI4NDQxZTYzMGMifQ.eyJleHAiOjE2NDI1NTM0NzUsImlhdCI6MTY0MjU1MTY3NSwianRpIjoiNTI1MDE1NTMtNzFmZi00NjI0LTg0NDItMmMzNzY3NGRkYjNkIiwiaXNzIjoiaHR0cDovL2hvc3QuZG9ja2VyLmludGVybmFsOjgwODAvYXV0aC9yZWFsbXMvZnVsbGN5Y2xlIiwiYXVkIjoiaHR0cDovL2hvc3QuZG9ja2VyLmludGVybmFsOjgwODAvYXV0aC9yZWFsbXMvZnVsbGN5Y2xlIiwic3ViIjoiNmYxODkxMTYtYTU0Mi00MzZiLWJhNWQtNDA1MjJjMWE2YWUwIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6Im5lc3QiLCJzZXNzaW9uX3N0YXRlIjoiODI5MTJjZWEtMzI4ZC00Y2ZkLTg1NTQtZWU2Y2RmMWUyMzQ5Iiwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiODI5MTJjZWEtMzI4ZC00Y2ZkLTg1NTQtZWU2Y2RmMWUyMzQ5In0.t8L9O_v7KW1akisFYiEntUI6kcYB9Y9Lo_kXtGUB950",

"token_type": "Bearer",

"not-before-policy": 0,

"session_state": "82912cea-328d-4cfd-8554-ee6cdf1e2349",

"scope": "email profile"

KeyCloak Public Key

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjBojl/HQ8J0BXCtLTnX0hQBLfIflbPclukIFwrFQ2JY9wSACXpOhO2vC6NLu02JO2r9z68VnxTgov8LuCArL/zzr4XZsOATK8bKdT6GI/bcsoCH0yJ0/CJ5go6KIOraQbsGI7rjWW/2If+5xfucJ4apiX1XpDAgKEOLV9tTCwMc+G7zPMFEiVZbS9HPI7BHPkYkHUmpR2K6klP7qSW9PnpFnGz1J6/vkP6yDUKYVkg7cUIV93rVcZvAXNGrLOmgvVAouLFFGgRGnKj+wdUFtRofVeOjYTnFwcot9P2wADQz8IkpD15NmY/l2PgB3uigRi7I83oWAwWVuFhNuxoCYcQIDAQAB

  • jwt-strategy.service.ts

import { Injectable } from '@nestjs/common';

import { PassportStrategy } from '@nestjs/passport';

import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()

export class JwtStrategyService extends PassportStrategy(Strategy, 'jwt') {

constructor() {

super({

jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),

ignoreExpiration: false,

secretOrKey: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjBojl/HQ8J0BXCtLTnX0hQBLfIflbPclukIFwrFQ2JY9wSACXpOhO2vC6NLu02JO2r9z68VnxTgov8LuCArL/zzr4XZsOATK8bKdT6GI/bcsoCH0yJ0/CJ5go6KIOraQbsGI7rjWW/2If+5xfucJ4apiX1XpDAgKEOLV9tTCwMc+G7zPMFEiVZbS9HPI7BHPkYkHUmpR2K6klP7qSW9PnpFnGz1J6/vkP6yDUKYVkg7cUIV93rVcZvAXNGrLOmgvVAouLFFGgRGnKj+wdUFtRofVeOjYTnFwcot9P2wADQz8IkpD15NmY/l2PgB3uigRi7I83oWAwWVuFhNuxoCYcQIDAQAB',

});

}

async validate(payload) {

return payload;

}

}

Environmental Variables

npm install @nestjs/config --save

  • .env

DB_CONNECTION=mysql
DB_HOST=db
DB_USERNAME=root
DB_PASSWORD=root
DB_DATABASE=fin
DB_PORT=3306

JWT_SECRET="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAszV9tJjxZ8LKOVcNRw74ZP5Z/ESEVsYYcVbZWkXupi5tEBoTiy3fEeZNTaQoAYZtsl3QIxD3+PPbNbqVa0JwtMo/qRWHT76f8r42eBbYl0GVhPmFMaP86G5/eOmlN4r4Lb2xNvqy5GlVwnNYd44kxgJfdmuMSdOaSVe9ksMHPVl7mVMoVwBAKbQa/YlukfUKAJwjAwbJZCknrPuQbgf3LKOIpo724eGNmC1yKx0ACvVCudvGuJ79KGeu64hi9wdpJu6SnRJdCn2g+K0uzQ4amIw8f5tUaYKKx2kMRd0FOYmr8Kg6Hda1JC49nbx7l/DbqcnZRDL5kHg8Km9v+6vi5QIDAQAB\n-----END PUBLIC KEY-----"

Validation Token from Keycloak with NESTJS

HTTP/1.1 200 OK

X-Powered-By: Express

Content-Type: application/json; charset=utf-8

Content-Length: 41

ETag: W/"29-YaKZtQQcSwG1X/7pJH1a82D0fjY"

Date: Wed, 19 Jan 2022 04:41:19 GMT

Connection: close

{

"name": "Luiz Carlos",

"route": "Security"

}

8. Multi-Tenancy Implementation

KeyCloak Implementation Multi-Tenant

  • One KeyCloak Instance for Each Group of Users (no Share)
  • One Realm of KeyCloak for Each Group of Users (Share Level One)
  • One Realm of KeyCloak for All Group of Users (Share Level Two)

KeyCloak with One Realm

KeyCloak with Two Realms

Example Multi-Tenant Architecture

KeyCloak Authentication Process

KeyCloak Authentication Plugins

Add Subdomain with KeyCloak

subdomain tenant1

KeyCloak Clients Setup

  • Mappers
  • subdomain

Token JWT with KeyCloak Subdomain

JWT Token with Subdomain KeyCloak

},
"scope": "email profile",
"sid": "bae21363-3d77-4f4a-ae35-2a84759f57a2",
"email_verified": false,
"name": "User1 Last Name1",
"subdomain": "tenant1", <<<<<<<<< Subdomain "tenant1"
"preferred_username": "user1@user.com",
"given_name": "User1",
"family_name": "Last Name1",
"email": "user1@user.com"
}

Case with Multi-Tenant

  • Financial Application
  • Tables
  • transactions = Bill to Pay, Bill to Receive, account_id
  • accounts = id, name, balance,subdomain

9. Setup MySQL Database with Docker

Use Dockerize with MySQL Container

FROM node:14.15.4-alpine3.12

RUN apk add --no-cache bash

RUN npm install -g @nestjs/cli@8.0.0

ENV DOCKERIZE_VERSION v0.6.1
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz

USER node

WORKDIR /home/node/app

  • docker-compose.yaml
  • Dockerize Wait DB MySQL Up

version: '3'

services:

app:

build: .

entrypoint: dockerize -wait tcp://db3306 -timeout 40s ./entrypoint.sh //<<<< Dockerize Wait DB MySQL Up

ports:

- 3000:3000

volumes:

- .:/home/node/app

extra_hosts:

- "host.docker.internal:172.17.0.1"

depends_on:

- db

db:

build: ./.docker/mysql

restart: always

tty: true

volumes:

- ./.docker/dbdata:/var/lib/mysql

environment:

- MYSQL_DATABASE=fin

- MYSQL_ROOT_PASSWORD=root

  • .env

# JWT_SECRET="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjBojl/HQ8J0BXCtLTnX0hQBLfIflbPclukIFwrFQ2JY9wSACXpOhO2vC6NLu02JO2r9z68VnxTgov8LuCArL/zzr4XZsOATK8bKdT6GI/bcsoCH0yJ0/CJ5go6KIOraQbsGI7rjWW/2If+5xfucJ4apiX1XpDAgKEOLV9tTCwMc+G7zPMFEiVZbS9HPI7BHPkYkHUmpR2K6klP7qSW9PnpFnGz1J6/vkP6yDUKYVkg7cUIV93rVcZvAXNGrLOmgvVAouLFFGgRGnKj+wdUFtRofVeOjYTnFwcot9P2wADQz8IkpD15NmY/l2PgB3uigRi7I83oWAwWVuFhNuxoCYcQIDAQAB"

JWT_SECRET="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjBojl/HQ8J0BXCtLTnX0hQBLfIflbPclukIFwrFQ2JY9wSACXpOhO2vC6NLu02JO2r9z68VnxTgov8LuCArL/zzr4XZsOATK8bKdT6GI/bcsoCH0yJ0/CJ5go6KIOraQbsGI7rjWW/2If+5xfucJ4apiX1XpDAgKEOLV9tTCwMc+G7zPMFEiVZbS9HPI7BHPkYkHUmpR2K6klP7qSW9PnpFnGz1J6/vkP6yDUKYVkg7cUIV93rVcZvAXNGrLOmgvVAouLFFGgRGnKj+wdUFtRofVeOjYTnFwcot9P2wADQz8IkpD15NmY/l2PgB3uigRi7I83oWAwWVuFhNuxoCYcQIDAQAB\n-----END PUBLIC KEY-----"

DB_CONNECTION=mysql

DB_HOST=db

DB_USERNAME=root

DB_PASSWORD=root

DB_DATABASE=fin

DB_PORT=3306

  • .docker/mysql/Dockerfile

FROM mysql:5.7

RUN usermod -u 1000 mysql

Docker Commands

docker-compose up --build

docker-compose up

10. Setup Sequelize ORM

https://sequelize.org/

  • Support NodeJS
  • Support NestJS
  • Support Typescript

docker-compose exec app bash

npm install @nestjs/sequelize sequelize sequelize-typescript

npm install @types/sequelize --save-dev

  • /src/app.module.ts

// import { HttpModule } from '@nestjs/axios';

import { Module } from '@nestjs/common';

import { ConfigModule } from '@nestjs/config';

import { AppController } from './app.controller';

import { AppService } from './app.service';

import { AuthModule } from './auth/auth.module';

//decorator - Javascript - Ecmascript 7

@Module({

imports: [

SequelizeModule.forRoot({

dialect: process.env.DB_CONNECTION as any,

host: process.env.DB_HOST,

port: parseInt(process.env.DB_PORT),

username: process.env.DB_USERNAME,

password: process.env.DB_PASSWORD,

database: process.env.DB_DATABASE,

models: [Transaction],

autoLoadModels: true,

synchronize: true,

sync: {

alter: true,

// force: true

},

}),

ConfigModule.forRoot({isGlobal:true}),

AuthModule],

controllers: [AppController],

providers: [AppService],

})

export class AppModule {}

Generation Transactions Module

nest g resource

Transaction Entity

  • Install MySQL2

npm install mysql2 --save

  • /transactions/entities/transaction.entity.ts

import { Model, Column, Table } from 'sequelize-typescript';

@Table({tableName:'transactions'})

export class Transaction extends Model {

@Column

payment_date: Date;

@Column

name: string;

@Column

amount: number;

@Column

subdomain: string;

}

Transactions:

  • Method POST

POST http://localhost:3000/transactions

Content-Type: application/json

{

"payment_date":"2021-01-01",

"name":" Nova Conta1",

"amount": 30

}

HTTP/1.1 201 Created

X-Powered-By: Express

Content-Type: application/json; charset=utf-8

Content-Length: 162

ETag: W/"a2-gMuY8B9fKvcXfE+grN+WpL343KY"

Date: Wed, 19 Jan 2022 23:53:06 GMT

Connection: close

{

"id": 1,

"payment_date": "2021-01-01T00:00:00.000Z",

"name": " Nova Conta1",

"amount": 30,

"updatedAt": "2022-01-19T23:53:06.120Z",

"createdAt": "2022-01-19T23:53:06.120Z"

}

  • Method GET

GET http://localhost:3000/transactions

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJZamg3TDNjbzhHNlU4Ykh0Rlg4Q3h1Y3phY0JEY0ZiMHJCRWhCSmRCTzQ4In0.eyJleHAiOjE2NDI2NzI2NjAsImlhdCI6MTY0MjYzNjY2MCwianRpIjoiMTM1NTgwMmItYjhmMy00YTBkLWI2YzItYjQwYTIwZDlmNmI5IiwiaXNzIjoiaHR0cDovL2hvc3QuZG9ja2VyLmludGVybmFsOjgwODAvYXV0aC9yZWFsbXMvZnVsbGN5Y2xlIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjZmMTg5MTE2LWE1NDItNDM2Yi1iYTVkLTQwNTIyYzFhNmFlMCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im5lc3QiLCJzZXNzaW9uX3N0YXRlIjoiZWI1MDlkNTAtYjNhNi00NmVjLThkNDktYWRiZmZmMzAyNTU4IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjMwMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJnZXJlbnRlIiwiZGVmYXVsdC1yb2xlcy1mdWxsY3ljbGUiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiJlYjUwOWQ1MC1iM2E2LTQ2ZWMtOGQ0OS1hZGJmZmYzMDI1NTgiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJVc2VyMSBMYXN0IE5hbWUxIiwic3ViZG9tYWluIjoidGVuYW50MSIsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIxQHVzZXIuY29tIiwiZ2l2ZW5fbmFtZSI6IlVzZXIxIiwiZmFtaWx5X25hbWUiOiJMYXN0IE5hbWUxIiwiZW1haWwiOiJ1c2VyMUB1c2VyLmNvbSJ9.Iz8i-qPYKejaEmXbyfyzVjWicA-BsYqfgAJdH09MntJMWVosNXBaVp9JOzKtUvh7Om_YGk4mDoR32TbLqlnv7o3XAROMDe6MulPyiU9GEkzQBSleb1FqH74Xm58FXQ0JtVb5XOwIGwjIFXvlcohf20AB-gl8uCZNr527Za2n00y5UpsHb85xUK326ZWOnFNdReeuz0w6A9ILzF5mbLuYJcI1_yNtUlXNzNqMQ4vcqtDXPE19W3xPqRHGlwucGYhaAbv0ozi4DwzRupmmQTwMhn3-c0V2fM4O8xkCJNMZJgmqirQF0JSnYkDVLGMLUkEXVfzvGTDon0eAQQUUP7TsUg

11.Create NestJS Module Mult Tenant

docker-compose exec app bash

nest g module tenant

nest g service tenant/tenant

NestJS Guardian

nest g guard tenant/tenant

docker-compose exec app bash

rm -rf dist

docker-compose up --build

  • tenant.module.ts

import { TenantGuard } from './tenant.guard';

import { Module, Global } from '@nestjs/common';

import { TenantService } from './tenant/tenant.service';

@Global()

@Module({

providers: [TenantService, TenantGuard],

exports: [TenantService],

})

export class TenantModule {}

Mult Tenant with Subdomain

Problems Resolution

docker container run -p 8443:84  
43 -d -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin jboss/keycloak

13. H2 Database Setup

Setup Database H2 in Docker Compose

A bit cleaner workaround is set the location using the GEONETWORK_DB_NAME env var, e.g. we have ours set to a separate location like so:

`**GEONETWORK_DB_NAME=/var/lib/geonetwork_db/gn**` **results in a db created at** `**/var/lib/geonetwork_db/gn.h2.db**`
  • docker-compose.yaml

environment:

- KEYCLOAK_USER=biolabs

- KEYCLOAK_PASSWORD=01042021

#- KEYCLOACK_IMPORT=/tmp/test-realm-export.json

- DB_VENDOR=h2

- GEONETWORK_DB_NAME=/var/lib/geonetwork_db/gn

ports:

- 8080:8080

#- 8443:8443

14. Keycloak email Setup

Steps

Assign email address to admin account

Use Keycloak Account Management to add email address in Personal Info
The below steps work for Keycloak 13 but UI may change with time

  • Login to Keycloak Security Admin Console using admin credentials
  • Click admin name shown in the top right corner
  • Click Manage account
  • Click Personal Info
  • Enter email address

Configure Email Settings

  • Open a realm
  • Under Realm Settings > Email the following details will work for a Gmail account
  • Host: smtp.gmail.com
  • Port: 587 (for SSL, use 465)
  • From: admin-email-address
  • Enable StartTLS: On (for SSL, use Enable SSL)
  • Enable Authentication: On
  • Username: username
  • Password: password

Configure Gmail

If the admin account is a Gmail account, the below steps are required

15. MySQL Integration

Docker Compose

version: '3'

volumes:

mysql_data:

driver: local

services:

keycloak:

image: quay.io/keycloak/keycloak:16.1.1

environment:

DB_VENDOR: MYSQL

DB_ADDR: mysql

DB_DATABASE: keycloak

DB_USER: keycloak

DB_PASSWORD: password

KEYCLOAK_USER: admin

KEYCLOAK_PASSWORD: admin

ports:

- 8085:8080

depends_on:

- mysql

mysql:

image: mysql:5.7

ports:

- 3306:3306

volumes:

- mysql_data:/var/lib/mysql

environment:

MYSQL_ROOT_PASSWORD: root

MYSQL_DATABASE: keycloak

MYSQL_USER: keycloak

MYSQL_PASSWORD: password

KeyCloak MySQL Database

16. Migration Database

From: H2 Database

To: MySQL

Migration Database From H2 to MySQL

17. KeyCloak Tests

  • Login
  • Logout
  • Refresh
  • ID Token JSON
  • Access Token Json
  • ID Token
  • Access Token
  • Refresh Token

References