Understanding the Powerful Keycloak Tool

Andre Vianna
My Dev Zone
Published in
10 min readJan 19, 2022

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/dataenvironment:- KEYCLOAK_USER=admin- KEYCLOAK_PASSWORD=admin#- KEYCLOACK_IMPORT=/tmp/test-realm-export.json- DB_VENDOR=h2ports:- 8080:8080#Start Docker Main Deamon
sudo dockerd &
#Download & Up Docker Image
docker-compose up
# localhost:8080docker 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
# Authhttp://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

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/tokenContent-Type: application/x-www-form-urlencodedclient_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_yco2Wc4fSo5jOasB3MyPbIhttps://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/tokenContent-Type: application/x-www-form-urlencodedclient_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/userinfoAuthorization: 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.shports:- 3000:3000volumes:- .:/home/node/app
  • Docker Compose Up
docker-compose updocker-compose exec app bashnpm install @nestjs/axios --save

6. ReactiveX

An API for asynchronous programming
with observable streams

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.shports:- 3000:3000volumes:- .:/home/node/appextra_hosts:- "host.docker.internal:172.17.0.1"//etc/hosts127.0.0.1 host.docker.internalKeyCloak
Port: 8080
  • /etc/hosts
  • Windows 10
  • aa
C:\Windows\System32\drivers\etc\hosts
HTTP/1.1 201 CreatedX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 2370ETag: W/"942-K7GG7R7oh3DcCQW7N3JdyLFcgkA"Date: Wed, 19 Jan 2022 00:21:15 GMTConnection: 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 OKX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 41ETag: W/"29-YaKZtQQcSwG1X/7pJH1a82D0fjY"Date: Wed, 19 Jan 2022 04:41:19 GMTConnection: 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.12RUN 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 Upports:- 3000:3000volumes:- .:/home/node/appextra_hosts:- "host.docker.internal:172.17.0.1"depends_on:- dbdb:build: ./.docker/mysqlrestart: alwaystty: truevolumes:- ./.docker/dbdata:/var/lib/mysqlenvironment:- 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=mysqlDB_HOST=dbDB_USERNAME=rootDB_PASSWORD=rootDB_DATABASE=finDB_PORT=3306
  • .docker/mysql/Dockerfile
FROM mysql:5.7RUN usermod -u 1000 mysql

Docker Commands

docker-compose up --builddocker-compose up 

10. Setup Sequelize ORM

https://sequelize.org/
  • Support NodeJS
  • Support NestJS
  • Support Typescript
docker-compose exec app bashnpm install @nestjs/sequelize sequelize sequelize-typescriptnpm 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 {@Columnpayment_date: Date;@Columnname: string;@Columnamount: number;@Columnsubdomain: string;}

Transactions:

  • Method POST
POST http://localhost:3000/transactionsContent-Type: application/json{"payment_date":"2021-01-01","name":" Nova Conta1","amount": 30}HTTP/1.1 201 CreatedX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 162ETag: W/"a2-gMuY8B9fKvcXfE+grN+WpL343KY"Date: Wed, 19 Jan 2022 23:53:06 GMTConnection: 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/transactionsAuthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJZamg3TDNjbzhHNlU4Ykh0Rlg4Q3h1Y3phY0JEY0ZiMHJCRWhCSmRCTzQ4In0.eyJleHAiOjE2NDI2NzI2NjAsImlhdCI6MTY0MjYzNjY2MCwianRpIjoiMTM1NTgwMmItYjhmMy00YTBkLWI2YzItYjQwYTIwZDlmNmI5IiwiaXNzIjoiaHR0cDovL2hvc3QuZG9ja2VyLmludGVybmFsOjgwODAvYXV0aC9yZWFsbXMvZnVsbGN5Y2xlIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjZmMTg5MTE2LWE1NDItNDM2Yi1iYTVkLTQwNTIyYzFhNmFlMCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im5lc3QiLCJzZXNzaW9uX3N0YXRlIjoiZWI1MDlkNTAtYjNhNi00NmVjLThkNDktYWRiZmZmMzAyNTU4IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjMwMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJnZXJlbnRlIiwiZGVmYXVsdC1yb2xlcy1mdWxsY3ljbGUiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiJlYjUwOWQ1MC1iM2E2LTQ2ZWMtOGQ0OS1hZGJmZmYzMDI1NTgiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJVc2VyMSBMYXN0IE5hbWUxIiwic3ViZG9tYWluIjoidGVuYW50MSIsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIxQHVzZXIuY29tIiwiZ2l2ZW5fbmFtZSI6IlVzZXIxIiwiZmFtaWx5X25hbWUiOiJMYXN0IE5hbWUxIiwiZW1haWwiOiJ1c2VyMUB1c2VyLmNvbSJ9.Iz8i-qPYKejaEmXbyfyzVjWicA-BsYqfgAJdH09MntJMWVosNXBaVp9JOzKtUvh7Om_YGk4mDoR32TbLqlnv7o3XAROMDe6MulPyiU9GEkzQBSleb1FqH74Xm58FXQ0JtVb5XOwIGwjIFXvlcohf20AB-gl8uCZNr527Za2n00y5UpsHb85xUK326ZWOnFNdReeuz0w6A9ILzF5mbLuYJcI1_yNtUlXNzNqMQ4vcqtDXPE19W3xPqRHGlwucGYhaAbv0ozi4DwzRupmmQTwMhn3-c0V2fM4O8xkCJNMZJgmqirQF0JSnYkDVLGMLUkEXVfzvGTDon0eAQQUUP7TsUg

11.Create NestJS Module Mult Tenant

docker-compose exec app bashnest g module tenant
nest g service tenant/tenant

NestJS Guardian

nest g guard tenant/tenant
docker-compose exec app bashrm -rf distdocker-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/gnports:- 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: localservices:keycloak:image: quay.io/keycloak/keycloak:16.1.1environment:DB_VENDOR: MYSQLDB_ADDR: mysqlDB_DATABASE: keycloakDB_USER: keycloakDB_PASSWORD: passwordKEYCLOAK_USER: adminKEYCLOAK_PASSWORD: adminports:- 8085:8080depends_on:- mysqlmysql:image: mysql:5.7ports:- 3306:3306volumes:- mysql_data:/var/lib/mysqlenvironment:MYSQL_ROOT_PASSWORD: rootMYSQL_DATABASE: keycloakMYSQL_USER: keycloakMYSQL_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

--

--

Andre Vianna
My Dev Zone

Software Engineer & Data Scientist #ESG #Vision2030 #Blockchain #DataScience #iot #bigdata #analytics #machinelearning #deeplearning #dataviz