Understanding the Powerful Keycloak Tool

Authentication Server with NestJS
Index
- Tutorial
- Installation
- Setup
- Add a New Realm
- OpenID Connect
- Integration NestJS + KeyCloak
- ReactiveX
- Internal Router Docker
- Multi-Tenancy Implementation
- Setup MySQL Database with Docker
- Setup Sequelize ORM
- Create NestJS Module Mult Tenant
- H2 Database Setup
- Keycloak email Setup
- MySQL Integration
- Migration Database
- Keycloak Tests
- 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/management
3. Setup KeyCloak

KeyCloak

add a New Realm

OAuth2 Concepts

Resource Owner
- Spotfy — Luiz
Client
- Application Spotify
Resource Server
Authorization Server
- 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

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
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
- Users
- Attributes
- http://127.0.0.1:8080/auth/admin/master/console/#/realms/fullcycle/users/6f189116-a542-436b-ba5d-40522c1a6ae0/user-attributes

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
- /nest/Dockerfile
- https://github.com/Backend-2030/live-multi-tenancy-nest-next-keycloak/blob/main/nest/Dockerfile
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

- 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
- Login to the Gmail account in a browser
- Visit https://www.google.com/settings/security/lesssecureapps
- Change the setting to On
- Visit https://accounts.google.com/DisplayUnlockCaptcha
- Follow on-screen instructions, if any
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





