Understanding the Powerful Keycloak Tool
Published in
10 min readJan 19, 2022
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
1.1.2 KeyCloak API Test
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 imagesdocker 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
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
Add KeyCloak Client
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
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
b) 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
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)
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
KeyCloak Clients Setup
- Mappers
- subdomain
Token JWT with KeyCloak Subdomain
},
"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
- /nest/Dockerfile
- https://github.com/Backend-2030/live-multi-tenancy-nest-next-keycloak/blob/main/nest/Dockerfile
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
- 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
- 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: 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
16. Migration Database
From: H2 Database
To: MySQL
17. KeyCloak Tests
- Login
- Logout
- Refresh
- ID Token JSON
- Access Token Json
- ID Token
- Access Token
- Refresh Token