From f726a9883fc47e37c00e50d6d8cf619fcb94fa5f Mon Sep 17 00:00:00 2001 From: marcos-silva-rodrigues Date: Fri, 27 Jun 2025 13:21:59 -0300 Subject: [PATCH] FEAT: create user from identity provider --- docsfera.json | 85 ++---- package-lock.json | 247 +++++++++++++++++- package.json | 6 +- src/modules/auth/auth.controller.ts | 17 -- .../identity-provider/dto/sso-signin.dto.ts | 9 + .../identity-provider.controller.ts | 36 ++- .../identity-provider.module.ts | 5 +- .../identity-provider.service.ts | 96 +++++-- src/services/cache.service.ts | 23 ++ src/services/service.module.ts | 22 ++ 10 files changed, 436 insertions(+), 110 deletions(-) create mode 100644 src/modules/identity-provider/dto/sso-signin.dto.ts create mode 100644 src/services/cache.service.ts create mode 100644 src/services/service.module.ts diff --git a/docsfera.json b/docsfera.json index f34893a..59a458b 100644 --- a/docsfera.json +++ b/docsfera.json @@ -675,41 +675,6 @@ ] } }, - "/auth/idp/{name}/callback": { - "get": { - "operationId": "AuthController_callbackIdp", - "parameters": [ - { - "name": "dadosfera-lang", - "in": "header", - "required": false, - "schema": { - "enum": [ - "pt-br", - "en-us" - ], - "type": "string" - } - }, - { - "name": "name", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "Auth" - ] - } - }, "/connections": { "post": { "operationId": "ConnectionController_createConnection", @@ -5939,7 +5904,12 @@ } } } - } + }, + "security": [ + { + "access-token": [] + } + ] }, "get": { "operationId": "IdentityProviderController_getProviders", @@ -5955,6 +5925,29 @@ } } } + }, + "security": [ + { + "access-token": [] + } + ] + } + }, + "/identity-providers/callback": { + "get": { + "operationId": "IdentityProviderController_callbackIdp", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } } } }, @@ -5978,26 +5971,6 @@ } } }, - "/identity-providers/{name}/callback": { - "get": { - "operationId": "IdentityProviderController_callbackIdp", - "parameters": [ - { - "name": "name", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - } - } - }, "/network-policy": { "get": { "operationId": "NetworkPolicyController_getNetworks", diff --git a/package-lock.json b/package-lock.json index 3634cd5..63acbdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@aws-sdk/client-secrets-manager": "^3.414.0", "@dadosfera/dadosfera-logs": "^1.0.0-beta.4", "@dadosfera/protospack": "2.5.3", - "@dadosfera/protospack-v2": "file:../protospack-v2/dadosfera-protospack-v2-0.0.0.tgz", + "@dadosfera/protospack-v2": "3.38.0-beta.6", "@grpc/grpc-js": "^1.9.3", "@grpc/proto-loader": "^0.7.9", "@nestjs/cli": "^9.5.0", @@ -27,6 +27,8 @@ "@nestjs/swagger": "^6.3.0", "@nestjs/testing": "^9.4.3", "axios": "^0.27.2", + "cache-manager": "^5.1.4", + "cache-manager-ioredis-yet": "^1.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cron-parser": "^4.9.0", @@ -48,12 +50,14 @@ "passport-hubspot-oauth2": "^1.0.3", "passport-mailchimp": "^1.1.0", "puppeteer": "^24.7.2", + "redis": "^4.5.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.5.5", "swagger-ui-express": "^4.6.3" }, "devDependencies": { + "@types/cache-manager": "^4.0.6", "@types/express": "^4.17.17", "@types/express-session": "^1.18.1", "@types/jest": "27.0.2", @@ -1403,9 +1407,9 @@ } }, "node_modules/@dadosfera/protospack-v2": { - "version": "0.0.0", - "resolved": "file:../protospack-v2/dadosfera-protospack-v2-0.0.0.tgz", - "integrity": "sha512-gT6JX/mh57cc/f9ey3ZPZ03fYKCqTj6CewazOnkf1juSAON65IR/9pD8lx/KMMcc3mYUT3Z3yjVAmXjvN09jPw==", + "version": "3.38.0-beta.6", + "resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.38.0-beta.6.tgz", + "integrity": "sha512-nj60p/CCG8tTJ4xJCCsWJpoiiz6YqjFmKRhHlwWaMT2RXdOfhMwz1adNhbjaJAIlhXQ0rjb0wyZHzt5wAgtJvA==", "license": "ISC", "dependencies": { "@grpc/grpc-js": "^1.9.3", @@ -1603,6 +1607,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2972,6 +2982,65 @@ "node": ">=18" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -3657,6 +3726,13 @@ "@types/node": "*" } }, + "node_modules/@types/cache-manager": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.6.tgz", + "integrity": "sha512-8qL93MF05/xrzFm/LSPtzNEOE1eQF3VwGHAcQEylgp5hDSTe41jtFwbSYAPfyYcVa28y1vYSjIt0c1fLLUiC/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -5250,6 +5326,41 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "5.7.6", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.6.tgz", + "integrity": "sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.2.2", + "promise-coalesce": "^1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cache-manager-ioredis-yet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cache-manager-ioredis-yet/-/cache-manager-ioredis-yet-1.2.2.tgz", + "integrity": "sha512-o03N/tQxfFONZ1XLGgIxOFHuQQpjpRdnSAL1THG1YWZIVp1JMUfjU3ElSAjFN1LjbJXa55IpC8waG+VEoLUCUw==", + "deprecated": "With cache-manager v6 we now are using Keyv", + "license": "MIT", + "dependencies": { + "cache-manager": "^5.2.3", + "ioredis": "^5.3.2" + }, + "engines": { + "node": ">= 16.17.0" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -5527,6 +5638,15 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6040,6 +6160,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6815,6 +6944,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -7552,6 +7687,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8127,6 +8271,30 @@ "node": ">= 0.10" } }, + "node_modules/ioredis": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", + "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -9457,12 +9625,30 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "license": "MIT" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -10948,6 +11134,15 @@ "node": ">=0.4.0" } }, + "node_modules/promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -11336,6 +11531,44 @@ "node": ">= 0.10" } }, + "node_modules/redis": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", + "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.1", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.1.14", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", @@ -12023,6 +12256,12 @@ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "license": "MIT" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index d4710bc..56f91f5 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@aws-sdk/client-secrets-manager": "^3.414.0", "@dadosfera/dadosfera-logs": "^1.0.0-beta.4", "@dadosfera/protospack": "2.5.3", - "@dadosfera/protospack-v2": "3.38.0-beta.4", + "@dadosfera/protospack-v2": "3.38.0-beta.6", "@grpc/grpc-js": "^1.9.3", "@grpc/proto-loader": "^0.7.9", "@nestjs/cli": "^9.5.0", @@ -45,6 +45,8 @@ "@nestjs/swagger": "^6.3.0", "@nestjs/testing": "^9.4.3", "axios": "^0.27.2", + "cache-manager": "^5.1.4", + "cache-manager-ioredis-yet": "^1.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cron-parser": "^4.9.0", @@ -66,6 +68,7 @@ "passport-hubspot-oauth2": "^1.0.3", "passport-mailchimp": "^1.1.0", "puppeteer": "^24.7.2", + "redis": "^4.5.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.5.5", @@ -75,6 +78,7 @@ "multer": "1.4.5-lts.1" }, "devDependencies": { + "@types/cache-manager": "^4.0.6", "@types/express": "^4.17.17", "@types/express-session": "^1.18.1", "@types/jest": "27.0.2", diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 5fab2cd..8d8352e 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -421,21 +421,4 @@ export class AuthController { return this.authClient.resetUsers(body.users, metadata); } - @Get('/idp/:name/callback') - @HttpCode(HttpStatus.OK) - async callbackIdp(@Param('name') idpName: string, @Req() req: Request) { - this.logger.info('/auth/idp/:name/callback'); - - const { params } = req; - - if (!params || !params.code) { - this.logger.error('No code received from IDP'); - throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_RESPONSE); - } - - return { - ok: true - } - - } } diff --git a/src/modules/identity-provider/dto/sso-signin.dto.ts b/src/modules/identity-provider/dto/sso-signin.dto.ts new file mode 100644 index 0000000..d339a69 --- /dev/null +++ b/src/modules/identity-provider/dto/sso-signin.dto.ts @@ -0,0 +1,9 @@ +export class SsoSignInDto { + readonly nonce: string; + readonly codeVerifier: string; + readonly state: string; + readonly id: string; + readonly clientId: string; + readonly clientSecret: string; + readonly issuerUrl: string; +} \ No newline at end of file diff --git a/src/modules/identity-provider/identity-provider.controller.ts b/src/modules/identity-provider/identity-provider.controller.ts index 320d503..f92f377 100644 --- a/src/modules/identity-provider/identity-provider.controller.ts +++ b/src/modules/identity-provider/identity-provider.controller.ts @@ -19,6 +19,8 @@ import { PackTheMetadata } from 'src/utils/ PackTheMetadata'; import { ApiOkResponse } from '@nestjs/swagger'; import { CreateIdentityProvider, IdentityProviderListResponse, IdentityProviderResponse } from './dto/identity-provider.dto'; import { Request } from 'express'; +import ErrorCodes from 'src/utils/errorCodes'; +import { Authenticated } from 'src/decorators/authentication.decorator'; @Controller('identity-providers') export class IdentityProviderController { @@ -35,6 +37,7 @@ export class IdentityProviderController { @Post() @HttpCode(HttpStatus.OK) @ApiOkResponse({ type: IdentityProviderResponse }) + @Authenticated() async addIdentityProvider( @User() user: RequestUser, @Body() body: CreateIdentityProvider, @@ -53,6 +56,7 @@ export class IdentityProviderController { @Get() @HttpCode(HttpStatus.OK) @ApiOkResponse({ type: IdentityProviderListResponse }) + @Authenticated() async getProviders( @User() user: RequestUser, @Language() language: LanguageEnum, @@ -63,7 +67,27 @@ export class IdentityProviderController { language }) - return await this.identityProviderService.getList(metadata) + const result = await this.identityProviderService.getList(metadata) + return result + } + + @Get('/callback') + @HttpCode(HttpStatus.OK) + async callbackIdp(@Req() req: Request) { + this.logger.info('/callback'); + const { code, state } = req.query; + + if (!code) { + this.logger.error('No code received from IDP'); + throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_RESPONSE); + } + + if (!state) { + this.logger.error('No state received from IDP'); + throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_RESPONSE); + } + + return await this.identityProviderService.getTokenByIdp(req); } @Get(':id') @@ -78,14 +102,4 @@ export class IdentityProviderController { }; } - @Get('/:name/callback') - @HttpCode(HttpStatus.OK) - async callbackIdp(@Param('name') name: string, @Req() req: Request) { - this.logger.info('/openid - /:name/callback'); - - return { - name, - params: req.params - }; - } } diff --git a/src/modules/identity-provider/identity-provider.module.ts b/src/modules/identity-provider/identity-provider.module.ts index 7449ea4..97d6299 100644 --- a/src/modules/identity-provider/identity-provider.module.ts +++ b/src/modules/identity-provider/identity-provider.module.ts @@ -4,12 +4,13 @@ import { IdentityProviderService } from './identity-provider.service'; import { ClientsModule } from '@nestjs/microservices'; import { DucClient } from '../duc/client.config'; import DadosferaLogger from '@dadosfera/dadosfera-logs'; +import { ServicesModule } from 'src/services/service.module'; const client = new DucClient(); @Module({ - imports: [ClientsModule.register([client.providerOptions])], + imports: [ClientsModule.register([client.providerOptions]), ServicesModule], controllers: [IdentityProviderController], - providers: [IdentityProviderService, DadosferaLogger] + providers: [IdentityProviderService, DadosferaLogger], }) export class IdentityProviderModule {} diff --git a/src/modules/identity-provider/identity-provider.service.ts b/src/modules/identity-provider/identity-provider.service.ts index f34d0c9..f1dcf27 100644 --- a/src/modules/identity-provider/identity-provider.service.ts +++ b/src/modules/identity-provider/identity-provider.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { DucClient } from '../duc/client.config'; import { ClientGrpc } from '@nestjs/microservices'; import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc'; @@ -7,6 +7,8 @@ import { lastValueFrom } from 'rxjs'; import { IdentityProviderRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages'; import { Metadata } from '@grpc/grpc-js'; import { Issuer, generators } from 'openid-client'; +import { SsoSignInDto } from './dto/sso-signin.dto'; +import { CacheService } from 'src/services/cache.service'; import { Request } from 'express'; @Injectable() @@ -14,6 +16,7 @@ export class IdentityProviderService implements OnModuleInit { private identityProviderService: IdentityProviderProtoService; constructor( @Inject(DucClient.name) private readonly grpcClient: ClientGrpc, + private readonly cacheService: CacheService ) {} onModuleInit() { @@ -37,46 +40,101 @@ export class IdentityProviderService implements OnModuleInit { async loginIdentityProvider(id: string) { const idp = await lastValueFrom( - this.identityProviderService.FindIdentityProvider({ id }), + this.identityProviderService.FindIdentityProvider({ id}), ); - const issuer = await Issuer.discover(idp.issuerUrl); + const redirectUri = this.getCallbackUrl(); + const cognitoIssuerURL = this.getCognitoIssuerUrl(); + + const issuer = await Issuer.discover(cognitoIssuerURL); const client = new issuer.Client({ client_id: idp.clientId, client_secret: idp.clientSecret, - redirect_uris: [`http://localhost:3333/auth/idp/${idp.name}/callback`], + redirect_uris: [redirectUri], response_types: ['code'], }); const code_verifier: string = generators.codeVerifier(); const code_challenge: string = generators.codeChallenge(code_verifier); + const state = generators.state(); + const nonce = generators.nonce(); + + // Using state because it is returned in the callback + // and we can use it to retrieve the code_verifier and nonce + await this.cacheService.set(state, { + codeVerifier: code_verifier, + nonce, + id: idp.id, + state, + clientId: idp.clientId, + clientSecret: idp.clientSecret, + issuerUrl: cognitoIssuerURL, + }); + const url = client.authorizationUrl({ - scope: 'openid email profile', + scope: 'openid email', response_type: 'code', code_challenge, code_challenge_method: 'S256', + state, + nonce }); const idpUrl = url + '&identity_provider=' + idp.name return idpUrl; } - // async authorizeIdp(name: string, req: Request) { - // const client = await this.buildOpenIdClient(name); - // const params = client.callbackParams(req); - // const code_verifier = req.session.code_verifier; - // const tokenSet = await client.callback( - // `http://localhost:3333/openid/${name}/callback`, - // params, - // { code_verifier }, - // ); + private getCallbackUrl() { + const AUTH_CALLBACK = process.env.AUTH_CALLBACK; + return AUTH_CALLBACK; + } - // delete req.session.code_verifier; + private getCognitoIssuerUrl() { + const COGNITO_ISSUER_URL = process.env.COGNITO_ISSUER_URL; + return COGNITO_ISSUER_URL; + } - // console.log('received and validated tokens %j', tokenSet); - // console.log('validated ID Token claims %j', tokenSet.claims()); + async getTokenByIdp(req: Request) { + const ssoSign = await this.cacheService.get(req.query.state as string); - // return tokenSet.claims(); - // } + if (!ssoSign) { + throw new BadRequestException('SSO sign-in is expired or not found'); + } + + const callbackUrl = this.getCallbackUrl(); + const issuer = await Issuer.discover(ssoSign.issuerUrl); + const client = new issuer.Client({ + client_id: ssoSign.clientId, + client_secret: ssoSign.clientSecret, + redirect_uris: [callbackUrl], + response_types: ['code'], + }); + + if (ssoSign === null) { + throw new BadRequestException('SSO sign-in is expired or not found'); + } + + const params = client.callbackParams(req) + const tokenSet = await client.callback( + callbackUrl, + params, + { + nonce: ssoSign.nonce, + code_verifier: ssoSign.codeVerifier, + state: ssoSign.state, + } + ); + + await this.cacheService.delete(ssoSign.state); + + return await lastValueFrom( + this.identityProviderService.SignInUser({ + accessToken: tokenSet.access_token, + idToken: tokenSet.id_token, + refreshToken: tokenSet.refresh_token, + id: ssoSign.id + }), + ); + } } diff --git a/src/services/cache.service.ts b/src/services/cache.service.ts new file mode 100644 index 0000000..25de25b --- /dev/null +++ b/src/services/cache.service.ts @@ -0,0 +1,23 @@ +import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common'; +import { Cache } from 'cache-manager'; + +@Injectable() +export class CacheService { + + constructor( + @Inject(CACHE_MANAGER) + private readonly cacheManager: Cache, + ) {} + + async get(key: string): Promise { + return await this.cacheManager.get(key); + } + + async set(key: string, value: T): Promise { + await this.cacheManager.set(key, value); + } + + async delete(key: string) { + await this.cacheManager.del(key); + } +} diff --git a/src/services/service.module.ts b/src/services/service.module.ts new file mode 100644 index 0000000..d129eaf --- /dev/null +++ b/src/services/service.module.ts @@ -0,0 +1,22 @@ +import { CacheModule, Module } from '@nestjs/common'; +import { CacheService } from './cache.service'; +import { redisStore } from 'cache-manager-ioredis-yet'; + +@Module({ + imports: [ + CacheModule.registerAsync({ + useFactory: async () => ({ + store: await redisStore({ + ttl: 1000 * 60, //1 minute + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT && Number(process.env.REDIS_PORT), + db: process.env.REDIS_DATABASE && Number(process.env.REDIS_DATABASE), + keyPrefix: 'maestro:sso', + }), + }), + }), + ], + providers: [CacheService], + exports: [CacheService], +}) +export class ServicesModule {}