From b7b999ef4580b91d25f224f91ce613890113258e Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Tue, 19 Dec 2023 11:23:12 +0100 Subject: [PATCH 01/17] initial nestjs boilerplate --- apps/nestjs-api/src/app/app.module.ts | 2 ++ .../auth-entra-id.controller.spec.ts | 18 ++++++++++++++++++ .../auth-entra-id/auth-entra-id.controller.ts | 9 +++++++++ .../src/auth-entra-id/auth-entra-id.module.ts | 11 +++++++++++ .../auth-entra-id.service.spec.ts | 18 ++++++++++++++++++ .../src/auth-entra-id/auth-entra-id.service.ts | 4 ++++ 6 files changed, 62 insertions(+) create mode 100644 apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.spec.ts create mode 100644 apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.ts create mode 100644 apps/nestjs-api/src/auth-entra-id/auth-entra-id.module.ts create mode 100644 apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.spec.ts create mode 100644 apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.ts diff --git a/apps/nestjs-api/src/app/app.module.ts b/apps/nestjs-api/src/app/app.module.ts index 91f9256fc..2e1ecb7a0 100644 --- a/apps/nestjs-api/src/app/app.module.ts +++ b/apps/nestjs-api/src/app/app.module.ts @@ -2,6 +2,7 @@ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo' import { CacheModule, Module } from '@nestjs/common' import { EventEmitterModule } from '@nestjs/event-emitter' import { GraphQLModule } from '@nestjs/graphql' +import { AuthEntraIdModule } from '../auth-entra-id/auth-entra-id.module' import { AuthModule } from '../auth/auth.module' import { ConMenteeFavoritedMentorsModule } from '../con-mentee-favorited-mentors/con-mentee-favorited-mentors.module' import { ConMentoringSessionsModule } from '../con-mentoring-sessions/con-mentoring-sessions.module' @@ -41,6 +42,7 @@ import { AppService } from './app.service' SfApiModule, SalesforceRecordEventsListenerModule, AuthModule, + AuthEntraIdModule, ConProfilesModule, ConMentoringSessionsModule, ConMentorshipMatchesModule, diff --git a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.spec.ts b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.spec.ts new file mode 100644 index 000000000..38267f958 --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthEntraIdController } from './auth-entra-id.controller'; + +describe('AuthEntraIdController', () => { + let controller: AuthEntraIdController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthEntraIdController], + }).compile(); + + controller = module.get(AuthEntraIdController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.ts b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.ts new file mode 100644 index 000000000..fa501ec9c --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('auth') +export class AuthEntraIdController { + @Get('entra-id') + entraId() { + return 'test' + } +} diff --git a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.module.ts b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.module.ts new file mode 100644 index 000000000..159246829 --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common' +import { AuthEntraIdController } from './auth-entra-id.controller' +import { AuthEntraIdService } from './auth-entra-id.service' + +@Module({ + imports: [], + controllers: [AuthEntraIdController], + providers: [AuthEntraIdService, AuthEntraIdController], + exports: [AuthEntraIdController] +}) +export class AuthEntraIdModule {} diff --git a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.spec.ts b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.spec.ts new file mode 100644 index 000000000..62415ad96 --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { AuthEntraIdService } from './auth-entra-id.service' + +describe('AuthEntraIdService', () => { + let service: AuthEntraIdService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthEntraIdService], + }).compile() + + service = module.get(AuthEntraIdService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.ts b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.ts new file mode 100644 index 000000000..0a2ea985e --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common' + +@Injectable() +export class AuthEntraIdService {} From d6efe52c5891e829f6c16422bf76038b13e62949 Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Tue, 19 Dec 2023 11:31:54 +0100 Subject: [PATCH 02/17] add fe boilerplate --- .../redi-connect/src/pages/front/login/EntraLogin.tsx | 7 +++++++ apps/redi-connect/src/pages/front/login/Login.tsx | 9 +++++++++ apps/redi-connect/src/routes/routes__logged-out.tsx | 11 +++++++++++ 3 files changed, 27 insertions(+) create mode 100644 apps/redi-connect/src/pages/front/login/EntraLogin.tsx diff --git a/apps/redi-connect/src/pages/front/login/EntraLogin.tsx b/apps/redi-connect/src/pages/front/login/EntraLogin.tsx new file mode 100644 index 000000000..1aafcc7e1 --- /dev/null +++ b/apps/redi-connect/src/pages/front/login/EntraLogin.tsx @@ -0,0 +1,7 @@ +import Landing from '../../../components/templates/Landing' + +export default function EntraLogin() { + return ( + + ) +} \ No newline at end of file diff --git a/apps/redi-connect/src/pages/front/login/Login.tsx b/apps/redi-connect/src/pages/front/login/Login.tsx index ed7e1267d..5543c1a80 100644 --- a/apps/redi-connect/src/pages/front/login/Login.tsx +++ b/apps/redi-connect/src/pages/front/login/Login.tsx @@ -291,6 +291,15 @@ export default function Login() { + + + Login with Microsoft + + + From b0561b5688a96ca0dfd63840d440e5814a91e3df Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Tue, 19 Dec 2023 14:10:28 +0100 Subject: [PATCH 05/17] enable cors in dev env --- apps/nestjs-api/src/main.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/nestjs-api/src/main.ts b/apps/nestjs-api/src/main.ts index 50fecc6b0..c75f35216 100644 --- a/apps/nestjs-api/src/main.ts +++ b/apps/nestjs-api/src/main.ts @@ -10,6 +10,8 @@ import { AppModule } from './app/app.module' async function bootstrap() { const app = await NestFactory.create(AppModule) + if (process.env.NODE_ENV !== 'production') app.enableCors() + const globalPrefix = 'api' app.setGlobalPrefix(globalPrefix) app.use(json({ limit: '1mb' })) From 53c938a1f49015680d957f5d06bbebeb6bd7ccbf Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Tue, 19 Dec 2023 15:45:25 +0100 Subject: [PATCH 06/17] wip: preparing module with dependencies --- apps/nestjs-api/src/app/app.module.ts | 4 +- .../auth-entra-id.controller.spec.ts | 18 --------- .../auth-entra-id/auth-entra-id.controller.ts | 9 ----- .../src/auth-entra-id/auth-entra-id.module.ts | 11 ------ .../auth-entra-id/auth-entra-id.service.ts | 4 -- .../auth-entra-id/entra-id-config.provider.ts | 39 +++++++++++++++++++ .../entra-id-login-options.interface.ts | 5 +++ .../auth-entra-id/entra-id.controller.spec.ts | 18 +++++++++ .../src/auth-entra-id/entra-id.controller.ts | 12 ++++++ .../src/auth-entra-id/entra-id.module.ts | 12 ++++++ ...rvice.spec.ts => entra-id.service.spec.ts} | 10 ++--- .../src/auth-entra-id/entra-id.service.ts | 36 +++++++++++++++++ package.json | 1 + yarn.lock | 34 +++++++++++++++- 14 files changed, 162 insertions(+), 51 deletions(-) delete mode 100644 apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.spec.ts delete mode 100644 apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.ts delete mode 100644 apps/nestjs-api/src/auth-entra-id/auth-entra-id.module.ts delete mode 100644 apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.ts create mode 100644 apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts create mode 100644 apps/nestjs-api/src/auth-entra-id/entra-id-login-options.interface.ts create mode 100644 apps/nestjs-api/src/auth-entra-id/entra-id.controller.spec.ts create mode 100644 apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts create mode 100644 apps/nestjs-api/src/auth-entra-id/entra-id.module.ts rename apps/nestjs-api/src/auth-entra-id/{auth-entra-id.service.spec.ts => entra-id.service.spec.ts} (51%) create mode 100644 apps/nestjs-api/src/auth-entra-id/entra-id.service.ts diff --git a/apps/nestjs-api/src/app/app.module.ts b/apps/nestjs-api/src/app/app.module.ts index 2e1ecb7a0..28cb1d28f 100644 --- a/apps/nestjs-api/src/app/app.module.ts +++ b/apps/nestjs-api/src/app/app.module.ts @@ -2,7 +2,7 @@ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo' import { CacheModule, Module } from '@nestjs/common' import { EventEmitterModule } from '@nestjs/event-emitter' import { GraphQLModule } from '@nestjs/graphql' -import { AuthEntraIdModule } from '../auth-entra-id/auth-entra-id.module' +import { EntraIdModule } from '../auth-entra-id/entra-id.module' import { AuthModule } from '../auth/auth.module' import { ConMenteeFavoritedMentorsModule } from '../con-mentee-favorited-mentors/con-mentee-favorited-mentors.module' import { ConMentoringSessionsModule } from '../con-mentoring-sessions/con-mentoring-sessions.module' @@ -42,7 +42,7 @@ import { AppService } from './app.service' SfApiModule, SalesforceRecordEventsListenerModule, AuthModule, - AuthEntraIdModule, + EntraIdModule, ConProfilesModule, ConMentoringSessionsModule, ConMentorshipMatchesModule, diff --git a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.spec.ts b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.spec.ts deleted file mode 100644 index 38267f958..000000000 --- a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthEntraIdController } from './auth-entra-id.controller'; - -describe('AuthEntraIdController', () => { - let controller: AuthEntraIdController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AuthEntraIdController], - }).compile(); - - controller = module.get(AuthEntraIdController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.ts b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.ts deleted file mode 100644 index 50707e305..000000000 --- a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.controller.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Controller, Post } from '@nestjs/common'; - -@Controller('auth') -export class AuthEntraIdController { - @Post('entra-id') - entraId() { - return 'test' - } -} diff --git a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.module.ts b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.module.ts deleted file mode 100644 index 159246829..000000000 --- a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common' -import { AuthEntraIdController } from './auth-entra-id.controller' -import { AuthEntraIdService } from './auth-entra-id.service' - -@Module({ - imports: [], - controllers: [AuthEntraIdController], - providers: [AuthEntraIdService, AuthEntraIdController], - exports: [AuthEntraIdController] -}) -export class AuthEntraIdModule {} diff --git a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.ts b/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.ts deleted file mode 100644 index 0a2ea985e..000000000 --- a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@nestjs/common' - -@Injectable() -export class AuthEntraIdService {} diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts b/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts new file mode 100644 index 000000000..b4658ea1a --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts @@ -0,0 +1,39 @@ +import msal, { LogLevel } from '@azure/msal-node' +import { Injectable } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import { EntraIdLoginOptions } from './entra-id-login-options.interface' + +@Injectable() +export class EntraIdConfigProvider { + readonly options: EntraIdLoginOptions + readonly msalConfig: msal.Configuration + + constructor(configService: ConfigService) { + this.options = { + scopes: [], + redirectUri: configService.get('NX_ENTRA_ID_FRONTEND_URI') + '/front/login', + successRedirect: configService.get('NX_ENTRA_ID_FRONTEND_URI') + '/front/login/entra-login', + } + this.msalConfig = { + auth: { + clientId: configService.get('NX_ENTRA_ID_CLIENT_ID'), // 'Application (client) ID' of app registration in Azure portal - this value is a GUID + authority: configService.get( + 'NX_ENTRA_ID_CLOUD_INSTANCE' + '/consumers' + ), // Full directory URL, in the form of https://login.microsoftonline.com/ + clientSecret: configService.get('NX_ENTRA_ID_CLIENT_SECRET'), // Client secret generated from the app registration in Azure portal + }, + system: { + loggerOptions: { + loggerCallback(logLevel, message, containsPii) { + if (!containsPii) { + if (logLevel === LogLevel.Error) console.error(message) + else console.log(message) + } + }, + piiLoggingEnabled: false, + logLevel: LogLevel.Verbose, + }, + }, + } + } +} diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id-login-options.interface.ts b/apps/nestjs-api/src/auth-entra-id/entra-id-login-options.interface.ts new file mode 100644 index 000000000..9a233554e --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/entra-id-login-options.interface.ts @@ -0,0 +1,5 @@ +export interface EntraIdLoginOptions { + scopes: string[] + redirectUri: string + successRedirect: string +} \ No newline at end of file diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.spec.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.spec.ts new file mode 100644 index 000000000..ad88fe998 --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EntraIdController } from './entra-id.controller'; + +describe('EntraIdController', () => { + let controller: EntraIdController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [EntraIdController], + }).compile(); + + controller = module.get(EntraIdController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts new file mode 100644 index 000000000..fee0c25bd --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Post } from '@nestjs/common'; +import { EntraIdService } from './entra-id.service'; + +@Controller('auth') +export class EntraIdController { + constructor(private readonly entraIdService: EntraIdService) {} + + @Post('entra-id') + entraId() { + return this.entraIdService.loginUrl() + } +} diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts new file mode 100644 index 000000000..416242913 --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { EntraIdConfigProvider } from './entra-id-config.provider' +import { EntraIdController } from './entra-id.controller' +import { EntraIdService } from './entra-id.service' + +@Module({ + imports: [ConfigModule], + controllers: [EntraIdController], + providers: [EntraIdConfigProvider, EntraIdService], +}) +export class EntraIdModule {} diff --git a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.spec.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.service.spec.ts similarity index 51% rename from apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.spec.ts rename to apps/nestjs-api/src/auth-entra-id/entra-id.service.spec.ts index 62415ad96..e6b8c6ac4 100644 --- a/apps/nestjs-api/src/auth-entra-id/auth-entra-id.service.spec.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.service.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing' -import { AuthEntraIdService } from './auth-entra-id.service' +import { EntraIdService } from './entra-id.service' -describe('AuthEntraIdService', () => { - let service: AuthEntraIdService +describe('EntraIdService', () => { + let service: EntraIdService beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [AuthEntraIdService], + providers: [EntraIdService], }).compile() - service = module.get(AuthEntraIdService) + service = module.get(EntraIdService) }) it('should be defined', () => { diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts new file mode 100644 index 000000000..81484f29c --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts @@ -0,0 +1,36 @@ +import { + AuthorizationUrlRequest, + ConfidentialClientApplication, + CryptoProvider, +} from '@azure/msal-node' + +import { Injectable } from '@nestjs/common' +import { EntraIdConfigProvider } from './entra-id-config.provider' + +@Injectable() +export class EntraIdService { + cryptoProvider = new CryptoProvider() + + constructor(private readonly configProvider: EntraIdConfigProvider) {} + + async loginUrl() { + const authCodeUrlParameters: AuthorizationUrlRequest = { + state: this.cryptoProvider.base64Encode( + JSON.stringify({ + successRedirect: this.configProvider.options.successRedirect || '/', + }) + ), + scopes: this.configProvider.options.scopes || [], + redirectUri: this.configProvider.options.redirectUri, + } + + // By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit: + // https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes + new ConfidentialClientApplication(this.configProvider.msalConfig) + .getAuthCodeUrl(authCodeUrlParameters) + .then((response) => { + return response + }) + .catch((error) => console.error(JSON.stringify(error))) + } +} diff --git a/package.json b/package.json index c2313fb9a..b4c6d4501 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@sentry/react": "^7.47.0", "@travelerdev/nestjs-sentry-graphql": "^4.1.1", "apollo-server-express": "^3.6.7", + "@azure/msal-node": "^1.18.4", "async": "^3.2.3", "aws-sdk": "^2.418.0", "axios": "^0.21.1", diff --git a/yarn.lock b/yarn.lock index 0f36691fc..202bc3cf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,6 +63,20 @@ dependencies: xss "^1.0.8" +"@azure/msal-common@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-13.3.1.tgz#012465bf940d12375dc47387b754ccf9d6b92180" + integrity sha512-Lrk1ozoAtaP/cp53May3v6HtcFSVxdFrg2Pa/1xu5oIvsIwhxW6zSPibKefCOVgd5osgykMi5jjcZHv8XkzZEQ== + +"@azure/msal-node@^1.18.4": + version "1.18.4" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-1.18.4.tgz#c921b0447c92fb3b0cb1ebf5a9a76fcad2ec7c21" + integrity sha512-Kc/dRvhZ9Q4+1FSfsTFDME/v6+R2Y1fuMty/TfwqE5p9GTPw08BPbKgeWinE8JRHRp+LemjQbUZsn4Q4l6Lszg== + dependencies: + "@azure/msal-common" "13.3.1" + jsonwebtoken "^9.0.0" + uuid "^8.3.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -13534,6 +13548,22 @@ jsonwebtoken@8.5.1, jsonwebtoken@^8.2.0, jsonwebtoken@^8.5.1: ms "^2.1.1" semver "^5.6.0" +jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + jsprim@^1.2.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" @@ -18659,7 +18689,7 @@ semver@7.3.4: dependencies: lru-cache "^6.0.0" -semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: +semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -20746,7 +20776,7 @@ uuid@3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@8.3.2, uuid@^8.0.0, uuid@^8.3.2: +uuid@8.3.2, uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== From bf49abd155f9c6dba9f4d12bc2f53958079d8370 Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Wed, 20 Dec 2023 17:30:41 +0100 Subject: [PATCH 07/17] wip --- .../auth-entra-id/entra-id-config.provider.ts | 17 ++- .../entra-id-login-options.interface.ts | 1 + .../src/auth-entra-id/entra-id.controller.ts | 7 +- .../src/auth-entra-id/entra-id.module.ts | 3 +- .../src/auth-entra-id/entra-id.service.ts | 137 +++++++++++++++--- package.json | 1 + yarn.lock | 5 + 7 files changed, 145 insertions(+), 26 deletions(-) diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts b/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts index b4658ea1a..f49f62dce 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts @@ -5,21 +5,20 @@ import { EntraIdLoginOptions } from './entra-id-login-options.interface' @Injectable() export class EntraIdConfigProvider { - readonly options: EntraIdLoginOptions - readonly msalConfig: msal.Configuration + private readonly options: EntraIdLoginOptions + private readonly msalConfig: msal.Configuration constructor(configService: ConfigService) { this.options = { scopes: [], redirectUri: configService.get('NX_ENTRA_ID_FRONTEND_URI') + '/front/login', successRedirect: configService.get('NX_ENTRA_ID_FRONTEND_URI') + '/front/login/entra-login', + cloudInstance: configService.get('NX_ENTRA_ID_CLOUD_INSTANCE'), } this.msalConfig = { auth: { clientId: configService.get('NX_ENTRA_ID_CLIENT_ID'), // 'Application (client) ID' of app registration in Azure portal - this value is a GUID - authority: configService.get( - 'NX_ENTRA_ID_CLOUD_INSTANCE' + '/consumers' - ), // Full directory URL, in the form of https://login.microsoftonline.com/ + authority: configService.get('NX_ENTRA_ID_CLOUD_INSTANCE') + '/consumers', // Full directory URL, in the form of https://login.microsoftonline.com/ clientSecret: configService.get('NX_ENTRA_ID_CLIENT_SECRET'), // Client secret generated from the app registration in Azure portal }, system: { @@ -36,4 +35,12 @@ export class EntraIdConfigProvider { }, } } + + getMsalConfig(): msal.Configuration { + return {auth: { ...this.msalConfig.auth }, system: { ...this.msalConfig.system }} + } + + getOptions(): EntraIdLoginOptions { + return {...this.options} + } } diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id-login-options.interface.ts b/apps/nestjs-api/src/auth-entra-id/entra-id-login-options.interface.ts index 9a233554e..759088b8e 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id-login-options.interface.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id-login-options.interface.ts @@ -2,4 +2,5 @@ export interface EntraIdLoginOptions { scopes: string[] redirectUri: string successRedirect: string + cloudInstance: string } \ No newline at end of file diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts index fee0c25bd..9e1c835e4 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts @@ -1,4 +1,5 @@ -import { Controller, Post } from '@nestjs/common'; +import { Controller, Post, Req } from '@nestjs/common'; +import { Request } from 'express'; import { EntraIdService } from './entra-id.service'; @Controller('auth') @@ -6,7 +7,7 @@ export class EntraIdController { constructor(private readonly entraIdService: EntraIdService) {} @Post('entra-id') - entraId() { - return this.entraIdService.loginUrl() + entraId(@Req() req: Request) { + return this.entraIdService.loginUrl(req) } } diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts index 416242913..ca5a829a7 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts @@ -1,3 +1,4 @@ +import { HttpModule } from '@nestjs/axios' import { Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' import { EntraIdConfigProvider } from './entra-id-config.provider' @@ -5,7 +6,7 @@ import { EntraIdController } from './entra-id.controller' import { EntraIdService } from './entra-id.service' @Module({ - imports: [ConfigModule], + imports: [ConfigModule, HttpModule], controllers: [EntraIdController], providers: [EntraIdConfigProvider, EntraIdService], }) diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts index 81484f29c..1e29b01c4 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts @@ -2,35 +2,138 @@ import { AuthorizationUrlRequest, ConfidentialClientApplication, CryptoProvider, + ResponseMode, } from '@azure/msal-node' +import { HttpService } from '@nestjs/axios' import { Injectable } from '@nestjs/common' +import { AxiosError, AxiosRequestConfig } from 'axios' +import { Request } from 'express' +import { firstValueFrom } from 'rxjs' +import { catchError } from 'rxjs/operators' import { EntraIdConfigProvider } from './entra-id-config.provider' @Injectable() export class EntraIdService { cryptoProvider = new CryptoProvider() - constructor(private readonly configProvider: EntraIdConfigProvider) {} + constructor( + private readonly configProvider: EntraIdConfigProvider, + private readonly httpService: HttpService + ) {} - async loginUrl() { - const authCodeUrlParameters: AuthorizationUrlRequest = { - state: this.cryptoProvider.base64Encode( - JSON.stringify({ - successRedirect: this.configProvider.options.successRedirect || '/', - }) - ), - scopes: this.configProvider.options.scopes || [], - redirectUri: this.configProvider.options.redirectUri, + async loginUrl(req: Request) { + const config = this.configProvider.getMsalConfig() + const options = this.configProvider.getOptions() + /** + * If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will + * make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making + * metadata discovery calls, thereby improving performance of token acquisition process. For more, see: + * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/performance.md + */ + if (!config.auth.cloudDiscoveryMetadata || !config.auth.authorityMetadata) { + const [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([ + this._getCloudDiscoveryMetadata(), + this._getAuthorityMetadata(), + ]) + + config.auth.cloudDiscoveryMetadata = JSON.stringify( + cloudDiscoveryMetadata + ) + config.auth.authorityMetadata = JSON.stringify(authorityMetadata) } - // By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit: - // https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes - new ConfidentialClientApplication(this.configProvider.msalConfig) - .getAuthCodeUrl(authCodeUrlParameters) - .then((response) => { - return response + /** + * MSAL Node library allows you to pass your custom state as state parameter in the Request object. + * The state parameter can also be used to encode information of the app's state before redirect. + * You can pass the user's state in the app, such as the page or view they were on, as input to this parameter. + */ + const state: string = this.cryptoProvider.base64Encode( + JSON.stringify({ + successRedirect: options.successRedirect || '/', }) - .catch((error) => console.error(JSON.stringify(error))) + ) + + const authCodeUrlRequestParams: AuthorizationUrlRequest = { + state: state, + /** + * By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit: + * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes + */ + scopes: options.scopes || [], + redirectUri: options.redirectUri, + } + + const authCodeRequestParams = { + state: state, + /** + * By default, MSAL Node will add OIDC scopes to the auth code request. For more information, visit: + * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes + */ + scopes: options.scopes || [], + redirectUri: options.redirectUri, + } + + const msalInstance = new ConfidentialClientApplication(config) + + // Generate PKCE Codes before starting the authorization flow + const { verifier, challenge } = + await this.cryptoProvider.generatePkceCodes() + + // Set generated PKCE codes and method as session vars + const pkceCodes = { + challengeMethod: 'S256', + verifier: verifier, + challenge: challenge, + } + + const authCodeUrlRequest = { + ...authCodeUrlRequestParams, + responseMode: ResponseMode.FORM_POST, // recommended for confidential clients + codeChallenge: pkceCodes.challenge, + codeChallengeMethod: pkceCodes.challengeMethod, + } + + const authCodeResponse = msalInstance.getAuthCodeUrl(authCodeUrlRequest) + return authCodeResponse + } + + // Retrieves cloud discovery metadata from the /discovery/instance endpoint + async _getCloudDiscoveryMetadata() { + const endpoint = `${ + this.configProvider.getOptions().cloudInstance + }/common/discovery/instance` + + return this._queryEndpoint( + endpoint, { + params: { + 'api-version': '1.1', + authorization_endpoint: `${ + this.configProvider.getMsalConfig().auth.authority + }/oauth2/v2.0/authorize`, + }, + }) + } + + // Retrieves oidc metadata from the openid endpoint + async _getAuthorityMetadata() { + const endpoint = `${ + this.configProvider.getMsalConfig().auth.authority + }/v2.0/.well-known/openid-configuration` + + return this._queryEndpoint(endpoint) + } + + async _queryEndpoint(endpoint: string, params?: AxiosRequestConfig) { + const { data } = await firstValueFrom( + this.httpService.get(endpoint, params).pipe( + catchError((error: AxiosError) => { + console.error(error.response.data) + throw 'Could not setup login service' + }) + ) + ) + + return data } } diff --git a/package.json b/package.json index b4c6d4501..c6089a9e5 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@nestjs/jwt": "^8.0.0", "@nestjs/passport": "^8.2.1", "@nestjs/platform-express": "^8.0.0", + "@nestjs/axios": "^3.0.1", "@react-pdf/renderer": "2.0.15", "@sentry/node": "^7.43.0", "@sentry/react": "^7.47.0", diff --git a/yarn.lock b/yarn.lock index 202bc3cf4..81116640b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2541,6 +2541,11 @@ lodash.omit "4.5.0" tslib "2.3.1" +"@nestjs/axios@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-3.0.1.tgz#b006f81dd54a49def92cfaf9a8970434567e75ce" + integrity sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ== + "@nestjs/common@^8.0.0": version "8.4.4" resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-8.4.4.tgz#0914c6c0540b5a344c7c8fd6072faa1a49af1158" From 794605041f3fe399ebde17308788d538291cc717 Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Thu, 21 Dec 2023 17:02:18 +0100 Subject: [PATCH 08/17] redirect to api --- .vscode/settings.json | 10 +- .../auth-entra-id/entra-id-config.provider.ts | 16 +- ...ervice.ts => entra-id-login.middleware.ts} | 159 +++++++++--------- .../auth-entra-id/entra-id.controller.spec.ts | 18 -- .../src/auth-entra-id/entra-id.controller.ts | 26 ++- .../src/auth-entra-id/entra-id.module.ts | 14 +- .../auth-entra-id/entra-id.service.spec.ts | 18 -- .../src/pages/front/login/EntraLogin.tsx | 1 - .../src/pages/front/login/Login.tsx | 6 +- .../src/routes/routes__logged-out.tsx | 9 + apps/redi-connect/src/services/api/api.tsx | 9 +- 11 files changed, 139 insertions(+), 147 deletions(-) rename apps/nestjs-api/src/auth-entra-id/{entra-id.service.ts => entra-id-login.middleware.ts} (55%) delete mode 100644 apps/nestjs-api/src/auth-entra-id/entra-id.controller.spec.ts delete mode 100644 apps/nestjs-api/src/auth-entra-id/entra-id.service.spec.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index ddc9ac195..fdba4738c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,7 +14,9 @@ } ] }, - "editor.codeActionsOnSave": { "source.organizeImports": true }, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, "explorer.fileNesting.patterns": { "*.ts": "${capture}.js, ${capture}.typegen.ts, ${capture}.graphql, ${capture}.generated.ts", "*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts", @@ -23,5 +25,9 @@ "tsconfig.json": "tsconfig.*.json", "package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml", "*.graphql": "${capture}.generated.ts" - } + }, + "cSpell.words": [ + "entra", + "msal" + ] } diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts b/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts index f49f62dce..c7892967e 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts @@ -5,14 +5,14 @@ import { EntraIdLoginOptions } from './entra-id-login-options.interface' @Injectable() export class EntraIdConfigProvider { - private readonly options: EntraIdLoginOptions - private readonly msalConfig: msal.Configuration + readonly options: EntraIdLoginOptions + readonly msalConfig: msal.Configuration constructor(configService: ConfigService) { this.options = { scopes: [], - redirectUri: configService.get('NX_ENTRA_ID_FRONTEND_URI') + '/front/login', - successRedirect: configService.get('NX_ENTRA_ID_FRONTEND_URI') + '/front/login/entra-login', + redirectUri: configService.get('NX_NESTJS_API_URI') + '/auth/entra-redirect', // to backend + successRedirect: configService.get('NX_FRONTEND_URI') + '/front/login/entra-login', // to frontend cloudInstance: configService.get('NX_ENTRA_ID_CLOUD_INSTANCE'), } this.msalConfig = { @@ -35,12 +35,4 @@ export class EntraIdConfigProvider { }, } } - - getMsalConfig(): msal.Configuration { - return {auth: { ...this.msalConfig.auth }, system: { ...this.msalConfig.system }} - } - - getOptions(): EntraIdLoginOptions { - return {...this.options} - } } diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts b/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts similarity index 55% rename from apps/nestjs-api/src/auth-entra-id/entra-id.service.ts rename to apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts index 1e29b01c4..ae5467c6c 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts @@ -1,30 +1,40 @@ import { AuthorizationUrlRequest, ConfidentialClientApplication, + Configuration, CryptoProvider, ResponseMode, } from '@azure/msal-node' import { HttpService } from '@nestjs/axios' -import { Injectable } from '@nestjs/common' +import { Injectable, NestMiddleware } from '@nestjs/common' import { AxiosError, AxiosRequestConfig } from 'axios' -import { Request } from 'express' +import { NextFunction, Request, Response } from 'express' import { firstValueFrom } from 'rxjs' import { catchError } from 'rxjs/operators' import { EntraIdConfigProvider } from './entra-id-config.provider' @Injectable() -export class EntraIdService { - cryptoProvider = new CryptoProvider() +export class EntraIdLoginMiddleware implements NestMiddleware { + private cryptoProvider = new CryptoProvider() + private authorizationUrlRequestParams: AuthorizationUrlRequest constructor( private readonly configProvider: EntraIdConfigProvider, private readonly httpService: HttpService - ) {} + ) { + this.authorizationUrlRequestParams = this._prepareAuthCodeRequestParams() + } + + use(_: Request, res: Response, next: NextFunction) { + this._prepareConfig().then((config) => { + this._redirectToAuthCodeUrl(config)(res, next) + }) + return null + } - async loginUrl(req: Request) { - const config = this.configProvider.getMsalConfig() - const options = this.configProvider.getOptions() + async _prepareConfig(): Promise { + const config = this.configProvider.msalConfig /** * If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will * make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making @@ -43,83 +53,58 @@ export class EntraIdService { config.auth.authorityMetadata = JSON.stringify(authorityMetadata) } - /** - * MSAL Node library allows you to pass your custom state as state parameter in the Request object. - * The state parameter can also be used to encode information of the app's state before redirect. - * You can pass the user's state in the app, such as the page or view they were on, as input to this parameter. - */ - const state: string = this.cryptoProvider.base64Encode( - JSON.stringify({ - successRedirect: options.successRedirect || '/', - }) - ) - - const authCodeUrlRequestParams: AuthorizationUrlRequest = { - state: state, - /** - * By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit: - * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes - */ - scopes: options.scopes || [], - redirectUri: options.redirectUri, - } - - const authCodeRequestParams = { - state: state, - /** - * By default, MSAL Node will add OIDC scopes to the auth code request. For more information, visit: - * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes - */ - scopes: options.scopes || [], - redirectUri: options.redirectUri, - } - - const msalInstance = new ConfidentialClientApplication(config) - - // Generate PKCE Codes before starting the authorization flow - const { verifier, challenge } = - await this.cryptoProvider.generatePkceCodes() - - // Set generated PKCE codes and method as session vars - const pkceCodes = { - challengeMethod: 'S256', - verifier: verifier, - challenge: challenge, - } + return config + } - const authCodeUrlRequest = { - ...authCodeUrlRequestParams, - responseMode: ResponseMode.FORM_POST, // recommended for confidential clients - codeChallenge: pkceCodes.challenge, - codeChallengeMethod: pkceCodes.challengeMethod, + _redirectToAuthCodeUrl(config: Configuration) { + return async (res: Response, next: NextFunction) => { + const msalInstance = new ConfidentialClientApplication(config) + + // Generate PKCE Codes before starting the authorization flow + const { verifier, challenge } = + await this.cryptoProvider.generatePkceCodes() + + // Set generated PKCE codes and method as session vars + const pkceCodes = { + challengeMethod: 'S256', + verifier: verifier, + challenge: challenge, + } + + const authCodeUrlRequest = { + ...this.authorizationUrlRequestParams, + responseMode: ResponseMode.FORM_POST, // recommended for confidential clients + codeChallenge: pkceCodes.challenge, + codeChallengeMethod: pkceCodes.challengeMethod, + } + + try { + const authCodeUrl = await msalInstance.getAuthCodeUrl( + authCodeUrlRequest + ) + console.info(authCodeUrl) + res.redirect(authCodeUrl) + } catch (error) { + next(error) + } } - - const authCodeResponse = msalInstance.getAuthCodeUrl(authCodeUrlRequest) - return authCodeResponse } // Retrieves cloud discovery metadata from the /discovery/instance endpoint async _getCloudDiscoveryMetadata() { - const endpoint = `${ - this.configProvider.getOptions().cloudInstance - }/common/discovery/instance` - - return this._queryEndpoint( - endpoint, { - params: { - 'api-version': '1.1', - authorization_endpoint: `${ - this.configProvider.getMsalConfig().auth.authority - }/oauth2/v2.0/authorize`, - }, - }) + const endpoint = `${this.configProvider.options.cloudInstance}/common/discovery/instance` + + return this._queryEndpoint(endpoint, { + params: { + 'api-version': '1.1', + authorization_endpoint: `${this.configProvider.msalConfig.auth.authority}/oauth2/v2.0/authorize`, + }, + }) } // Retrieves oidc metadata from the openid endpoint async _getAuthorityMetadata() { - const endpoint = `${ - this.configProvider.getMsalConfig().auth.authority - }/v2.0/.well-known/openid-configuration` + const endpoint = `${this.configProvider.msalConfig.auth.authority}/v2.0/.well-known/openid-configuration` return this._queryEndpoint(endpoint) } @@ -136,4 +121,28 @@ export class EntraIdService { return data } + + _prepareAuthCodeRequestParams(): AuthorizationUrlRequest { + const options = this.configProvider.options + /** + * MSAL Node library allows you to pass your custom state as state parameter in the Request object. + * The state parameter can also be used to encode information of the app's state before redirect. + * You can pass the user's state in the app, such as the page or view they were on, as input to this parameter. + */ + const state: string = this.cryptoProvider.base64Encode( + JSON.stringify({ + successRedirect: options.successRedirect || '/', + }) + ) + + return { + state: state, + /** + * By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit: + * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes + */ + scopes: options.scopes || [], + redirectUri: options.redirectUri, + } + } } diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.spec.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.spec.ts deleted file mode 100644 index ad88fe998..000000000 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { EntraIdController } from './entra-id.controller'; - -describe('EntraIdController', () => { - let controller: EntraIdController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [EntraIdController], - }).compile(); - - controller = module.get(EntraIdController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts index 9e1c835e4..e90199f6f 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts @@ -1,13 +1,25 @@ -import { Controller, Post, Req } from '@nestjs/common'; -import { Request } from 'express'; -import { EntraIdService } from './entra-id.service'; +import { Controller, Get, Next, Post, Req, Res } from '@nestjs/common'; +import { NextFunction, Request, Response } from 'express'; +import { EntraIdLoginMiddleware } from './entra-id-login.middleware'; @Controller('auth') export class EntraIdController { - constructor(private readonly entraIdService: EntraIdService) {} + constructor(private readonly entraIdService: EntraIdLoginMiddleware) {} - @Post('entra-id') - entraId(@Req() req: Request) { - return this.entraIdService.loginUrl(req) + @Get('entra-id') + entraId(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + return '' + } + + @Get('entra-redirect') + redirectGet(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + console.log('get entra-redirect', req) + return '' + } + + @Post('entra-redirect') + redirect(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + console.log('post entra-redirect', req) + return '' } } diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts index ca5a829a7..ce5a62d2e 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts @@ -1,13 +1,19 @@ import { HttpModule } from '@nestjs/axios' -import { Module } from '@nestjs/common' +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' import { EntraIdConfigProvider } from './entra-id-config.provider' +import { EntraIdLoginMiddleware } from './entra-id-login.middleware' import { EntraIdController } from './entra-id.controller' -import { EntraIdService } from './entra-id.service' @Module({ imports: [ConfigModule, HttpModule], controllers: [EntraIdController], - providers: [EntraIdConfigProvider, EntraIdService], + providers: [EntraIdConfigProvider, EntraIdLoginMiddleware], }) -export class EntraIdModule {} +export class EntraIdModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer + .apply(EntraIdLoginMiddleware) + .forRoutes('auth/entra-id'); + } +} diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.service.spec.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.service.spec.ts deleted file mode 100644 index e6b8c6ac4..000000000 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { EntraIdService } from './entra-id.service' - -describe('EntraIdService', () => { - let service: EntraIdService - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [EntraIdService], - }).compile() - - service = module.get(EntraIdService) - }) - - it('should be defined', () => { - expect(service).toBeDefined() - }) -}) diff --git a/apps/redi-connect/src/pages/front/login/EntraLogin.tsx b/apps/redi-connect/src/pages/front/login/EntraLogin.tsx index dccd950a6..8a1fe4bbd 100644 --- a/apps/redi-connect/src/pages/front/login/EntraLogin.tsx +++ b/apps/redi-connect/src/pages/front/login/EntraLogin.tsx @@ -2,7 +2,6 @@ import { Heading } from '@talent-connect/shared-atomic-design-components' import { useState } from 'react' import { Columns } from 'react-bulma-components' import Landing from '../../../components/templates/Landing' -import { getEntraLoginUrl } from '../../../services/api/api' export default function EntraLogin() { const [loginError, setLoginError] = useState('') diff --git a/apps/redi-connect/src/pages/front/login/Login.tsx b/apps/redi-connect/src/pages/front/login/Login.tsx index ee7a37f43..8417a8c5f 100644 --- a/apps/redi-connect/src/pages/front/login/Login.tsx +++ b/apps/redi-connect/src/pages/front/login/Login.tsx @@ -26,7 +26,7 @@ import * as Yup from 'yup' import { showNotification } from '../../../components/AppNotification' import Teaser from '../../../components/molecules/Teaser' import AccountOperation from '../../../components/templates/AccountOperation' -import { getEntraLoginUrl, login } from '../../../services/api/api' +import { login } from '../../../services/api/api' import { getAccessTokenFromLocalStorage, purgeAllSessionData, @@ -77,9 +77,9 @@ export default function Login() { const [tpProfileLocation, setTpProfileLocation] = useState(null) - const loginWithEntraId = async () => { + const loginWithEntraId = () => { try { - const url = await getEntraLoginUrl() + history.push('/front/login/entra-redirect') } catch (err) { setLoginError('Could not log in with Entra') } diff --git a/apps/redi-connect/src/routes/routes__logged-out.tsx b/apps/redi-connect/src/routes/routes__logged-out.tsx index 7d8eb46c3..773e635a4 100644 --- a/apps/redi-connect/src/routes/routes__logged-out.tsx +++ b/apps/redi-connect/src/routes/routes__logged-out.tsx @@ -5,6 +5,7 @@ import Mentee from '../pages/front/landing/Mentee' import Mentor from '../pages/front/landing/Mentor' import { RequestResetPasswordEmail } from '../pages/front/reset-password/RequestResetPasswordEmail' import { SetNewPassword } from '../pages/front/reset-password/SetNewPassword' +import { entraLoginUrl } from '../services/api/api' import { RouteDefinition } from './index' const Login = lazy( () => @@ -80,6 +81,14 @@ export const routes__loggedOut: RouteDefinition[] = [ component: EntraLogin, exact: true, }, + { + path: '/front/login/entra-redirect', + component: () => { + window.location.href = entraLoginUrl() + return null + }, + exact: true, + }, { path: '/front/signup-landing', component: SignUpLanding, diff --git a/apps/redi-connect/src/services/api/api.tsx b/apps/redi-connect/src/services/api/api.tsx index 42e37f446..78db6f9a4 100644 --- a/apps/redi-connect/src/services/api/api.tsx +++ b/apps/redi-connect/src/services/api/api.tsx @@ -51,13 +51,8 @@ export const logout = () => { history.push('/front/home') } -export const getEntraLoginUrl = async (): Promise => { - const res = await http(`${NEST_API_URL}/auth/entra-id`, { - method: 'post', - data: {}, - }) - - return res.data +export const entraLoginUrl = (): string => { + return `${NEST_API_URL}/auth/entra-id` } export const requestResetPasswordEmail = async (email: string) => { From 95cc31bccc1bed4bd2dffe97685cb9478082e72f Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Fri, 22 Dec 2023 15:20:55 +0100 Subject: [PATCH 09/17] refactor --- .../entra-id-login.middleware.ts | 122 ++++-------------- .../src/auth-entra-id/entra-id.controller.ts | 10 +- .../src/auth-entra-id/entra-id.module.ts | 3 +- .../src/auth-entra-id/entra-id.service.ts | 86 ++++++++++++ .../src/entra-id/entra-id.service.spec.ts | 18 +++ .../src/entra-id/entra-id.service.ts | 4 + 6 files changed, 143 insertions(+), 100 deletions(-) create mode 100644 apps/nestjs-api/src/auth-entra-id/entra-id.service.ts create mode 100644 apps/nestjs-api/src/entra-id/entra-id.service.spec.ts create mode 100644 apps/nestjs-api/src/entra-id/entra-id.service.ts diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts b/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts index ae5467c6c..d93c4a991 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts @@ -1,135 +1,67 @@ import { AuthorizationUrlRequest, - ConfidentialClientApplication, - Configuration, - CryptoProvider, - ResponseMode, + ResponseMode } from '@azure/msal-node' -import { HttpService } from '@nestjs/axios' import { Injectable, NestMiddleware } from '@nestjs/common' -import { AxiosError, AxiosRequestConfig } from 'axios' import { NextFunction, Request, Response } from 'express' -import { firstValueFrom } from 'rxjs' -import { catchError } from 'rxjs/operators' import { EntraIdConfigProvider } from './entra-id-config.provider' +import { EntraIdService } from './entra-id.service' @Injectable() export class EntraIdLoginMiddleware implements NestMiddleware { - private cryptoProvider = new CryptoProvider() private authorizationUrlRequestParams: AuthorizationUrlRequest constructor( - private readonly configProvider: EntraIdConfigProvider, - private readonly httpService: HttpService + private readonly idService: EntraIdService, + private readonly configProvider: EntraIdConfigProvider ) { - this.authorizationUrlRequestParams = this._prepareAuthCodeRequestParams() + this.authorizationUrlRequestParams = this.prepareAuthCodeRequestParams() } use(_: Request, res: Response, next: NextFunction) { - this._prepareConfig().then((config) => { - this._redirectToAuthCodeUrl(config)(res, next) - }) + this.redirectToAuthCodeUrl()(res, next) return null } - async _prepareConfig(): Promise { - const config = this.configProvider.msalConfig - /** - * If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will - * make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making - * metadata discovery calls, thereby improving performance of token acquisition process. For more, see: - * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/performance.md - */ - if (!config.auth.cloudDiscoveryMetadata || !config.auth.authorityMetadata) { - const [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([ - this._getCloudDiscoveryMetadata(), - this._getAuthorityMetadata(), - ]) - - config.auth.cloudDiscoveryMetadata = JSON.stringify( - cloudDiscoveryMetadata - ) - config.auth.authorityMetadata = JSON.stringify(authorityMetadata) - } - - return config - } - - _redirectToAuthCodeUrl(config: Configuration) { + private redirectToAuthCodeUrl() { return async (res: Response, next: NextFunction) => { - const msalInstance = new ConfidentialClientApplication(config) - // Generate PKCE Codes before starting the authorization flow - const { verifier, challenge } = - await this.cryptoProvider.generatePkceCodes() - - // Set generated PKCE codes and method as session vars - const pkceCodes = { - challengeMethod: 'S256', - verifier: verifier, - challenge: challenge, - } + const { challenge } = + await this.idService.cryptoProvider.generatePkceCodes() const authCodeUrlRequest = { ...this.authorizationUrlRequestParams, responseMode: ResponseMode.FORM_POST, // recommended for confidential clients - codeChallenge: pkceCodes.challenge, - codeChallengeMethod: pkceCodes.challengeMethod, - } - - try { - const authCodeUrl = await msalInstance.getAuthCodeUrl( - authCodeUrlRequest - ) - console.info(authCodeUrl) - res.redirect(authCodeUrl) - } catch (error) { - next(error) + codeChallenge: challenge, + codeChallengeMethod: 'S256', } - } - } - - // Retrieves cloud discovery metadata from the /discovery/instance endpoint - async _getCloudDiscoveryMetadata() { - const endpoint = `${this.configProvider.options.cloudInstance}/common/discovery/instance` - - return this._queryEndpoint(endpoint, { - params: { - 'api-version': '1.1', - authorization_endpoint: `${this.configProvider.msalConfig.auth.authority}/oauth2/v2.0/authorize`, - }, - }) - } - - // Retrieves oidc metadata from the openid endpoint - async _getAuthorityMetadata() { - const endpoint = `${this.configProvider.msalConfig.auth.authority}/v2.0/.well-known/openid-configuration` - return this._queryEndpoint(endpoint) - } - - async _queryEndpoint(endpoint: string, params?: AxiosRequestConfig) { - const { data } = await firstValueFrom( - this.httpService.get(endpoint, params).pipe( - catchError((error: AxiosError) => { - console.error(error.response.data) - throw 'Could not setup login service' + this.idService + .getClientApplication() + .then((a) => { + a.getAuthCodeUrl(authCodeUrlRequest) + .then((url) => res.redirect(url)) + .catch((err) => { + console.error(err) + next(err) + }) }) - ) - ) - - return data + .catch((err) => { + console.error(err) + next(err) + }) + } } - _prepareAuthCodeRequestParams(): AuthorizationUrlRequest { + private prepareAuthCodeRequestParams(): AuthorizationUrlRequest { const options = this.configProvider.options /** * MSAL Node library allows you to pass your custom state as state parameter in the Request object. * The state parameter can also be used to encode information of the app's state before redirect. * You can pass the user's state in the app, such as the page or view they were on, as input to this parameter. */ - const state: string = this.cryptoProvider.base64Encode( + const state: string = this.idService.cryptoProvider.base64Encode( JSON.stringify({ successRedirect: options.successRedirect || '/', }) diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts index e90199f6f..e08c14a1f 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts @@ -6,20 +6,22 @@ import { EntraIdLoginMiddleware } from './entra-id-login.middleware'; export class EntraIdController { constructor(private readonly entraIdService: EntraIdLoginMiddleware) {} + // empty route to trigger the entra-id auth middleware @Get('entra-id') - entraId(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + entraId() { return '' } @Get('entra-redirect') redirectGet(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - console.log('get entra-redirect', req) + console.log('get entra-redirect', req.body) return '' } @Post('entra-redirect') - redirect(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - console.log('post entra-redirect', req) + redirectPost(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + + console.log('post entra-redirect', req.body) return '' } } diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts index ce5a62d2e..28e254d14 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.module.ts @@ -4,11 +4,12 @@ import { ConfigModule } from '@nestjs/config' import { EntraIdConfigProvider } from './entra-id-config.provider' import { EntraIdLoginMiddleware } from './entra-id-login.middleware' import { EntraIdController } from './entra-id.controller' +import { EntraIdService } from './entra-id.service' @Module({ imports: [ConfigModule, HttpModule], controllers: [EntraIdController], - providers: [EntraIdConfigProvider, EntraIdLoginMiddleware], + providers: [EntraIdConfigProvider, EntraIdService, EntraIdLoginMiddleware], }) export class EntraIdModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts new file mode 100644 index 000000000..471950425 --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts @@ -0,0 +1,86 @@ +import { + ClientApplication, + ConfidentialClientApplication, + Configuration, + CryptoProvider, +} from '@azure/msal-node' +import { HttpService } from '@nestjs/axios' +import { Injectable } from '@nestjs/common' +import { AxiosError, AxiosRequestConfig } from 'axios' +import { firstValueFrom } from 'rxjs' +import { catchError } from 'rxjs/operators' +import { EntraIdConfigProvider } from './entra-id-config.provider' + +@Injectable() +export class EntraIdService { + cryptoProvider = new CryptoProvider() + + constructor( + private readonly configProvider: EntraIdConfigProvider, + private readonly httpService: HttpService + ) {} + + async getClientApplication(): Promise { + const config = await this.prepareConfig() + + return new Promise((resolve) => { + return resolve(new ConfidentialClientApplication(config)) + }) + } + + private async prepareConfig(): Promise { + const config = this.configProvider.msalConfig + + /** + * If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will + * make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making + * metadata discovery calls, thereby improving performance of token acquisition process. For more, see: + * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/performance.md + */ + if (!config.auth.cloudDiscoveryMetadata || !config.auth.authorityMetadata) { + const [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([ + this.getCloudDiscoveryMetadata(), + this.getAuthorityMetadata(), + ]) + + config.auth.cloudDiscoveryMetadata = JSON.stringify( + cloudDiscoveryMetadata + ) + config.auth.authorityMetadata = JSON.stringify(authorityMetadata) + } + + return config + } + + // Retrieves cloud discovery metadata from the /discovery/instance endpoint + private async getCloudDiscoveryMetadata() { + const endpoint = `${this.configProvider.options.cloudInstance}/common/discovery/instance` + + return this.queryEndpoint(endpoint, { + params: { + 'api-version': '1.1', + authorization_endpoint: `${this.configProvider.msalConfig.auth.authority}/oauth2/v2.0/authorize`, + }, + }) + } + + // Retrieves oidc metadata from the openid endpoint + private async getAuthorityMetadata() { + const endpoint = `${this.configProvider.msalConfig.auth.authority}/v2.0/.well-known/openid-configuration` + + return this.queryEndpoint(endpoint) + } + + private async queryEndpoint(endpoint: string, params?: AxiosRequestConfig) { + const { data } = await firstValueFrom( + this.httpService.get(endpoint, params).pipe( + catchError((error: AxiosError) => { + console.error(error.response.data) + throw 'Could not setup login service' + }) + ) + ) + + return data + } +} diff --git a/apps/nestjs-api/src/entra-id/entra-id.service.spec.ts b/apps/nestjs-api/src/entra-id/entra-id.service.spec.ts new file mode 100644 index 000000000..265098445 --- /dev/null +++ b/apps/nestjs-api/src/entra-id/entra-id.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EntraIdService } from './entra-id.service'; + +describe('EntraIdService', () => { + let service: EntraIdService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [EntraIdService], + }).compile(); + + service = module.get(EntraIdService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/nestjs-api/src/entra-id/entra-id.service.ts b/apps/nestjs-api/src/entra-id/entra-id.service.ts new file mode 100644 index 000000000..57f666cd6 --- /dev/null +++ b/apps/nestjs-api/src/entra-id/entra-id.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class EntraIdService {} From 88b2e847cf57cfbff6e2a9ad92bbf7e848876244 Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Sat, 23 Dec 2023 00:58:40 +0100 Subject: [PATCH 10/17] request body verification finally working --- .../entra-id-login.middleware.ts | 66 +++++++++---------- .../src/auth-entra-id/entra-id.controller.ts | 6 +- .../src/auth-entra-id/entra-id.service.ts | 60 ++++++++++++++++- .../verification-data.interface.ts | 7 ++ apps/nestjs-api/src/main.ts | 2 + package.json | 2 + yarn.lock | 20 ++++++ 7 files changed, 124 insertions(+), 39 deletions(-) create mode 100644 apps/nestjs-api/src/auth-entra-id/verification-data.interface.ts diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts b/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts index d93c4a991..5fe6db8f9 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts @@ -1,12 +1,10 @@ -import { - AuthorizationUrlRequest, - ResponseMode -} from '@azure/msal-node' +import { AuthorizationUrlRequest, ResponseMode } from '@azure/msal-node' import { Injectable, NestMiddleware } from '@nestjs/common' import { NextFunction, Request, Response } from 'express' import { EntraIdConfigProvider } from './entra-id-config.provider' import { EntraIdService } from './entra-id.service' +import { VerificationData } from './verification-data.interface' @Injectable() export class EntraIdLoginMiddleware implements NestMiddleware { @@ -20,38 +18,37 @@ export class EntraIdLoginMiddleware implements NestMiddleware { } use(_: Request, res: Response, next: NextFunction) { - this.redirectToAuthCodeUrl()(res, next) + this.redirectToAuthCodeUrl(res, next) return null } - private redirectToAuthCodeUrl() { - return async (res: Response, next: NextFunction) => { - // Generate PKCE Codes before starting the authorization flow - const { challenge } = - await this.idService.cryptoProvider.generatePkceCodes() + private async redirectToAuthCodeUrl(res: Response, next: NextFunction) { + // Generate PKCE Codes before starting the authorization flow + const { verifier, challenge } = await this.idService.generatePkceCodes() - const authCodeUrlRequest = { - ...this.authorizationUrlRequestParams, - responseMode: ResponseMode.FORM_POST, // recommended for confidential clients - codeChallenge: challenge, - codeChallengeMethod: 'S256', - } + const verificationData = { + ...this.authorizationUrlRequestParams, + code: '', + codeVerifier: verifier + } as VerificationData - this.idService - .getClientApplication() - .then((a) => { - a.getAuthCodeUrl(authCodeUrlRequest) - .then((url) => res.redirect(url)) - .catch((err) => { - console.error(err) - next(err) - }) - }) - .catch((err) => { - console.error(err) - next(err) - }) + res.cookie(this.idService.verifierCookieName, this.idService.encodeObject(verificationData)) + + const authCodeUrlRequest = { + ...this.authorizationUrlRequestParams, + responseMode: ResponseMode.FORM_POST, // recommended for confidential clients + codeChallenge: challenge, + codeChallengeMethod: 'S256', } + + const clientApplication = await this.idService.getClientApplication() + clientApplication + .getAuthCodeUrl(authCodeUrlRequest) + .then((url) => res.redirect(url)) + .catch((err) => { + console.error(err) + next(err) + }) } private prepareAuthCodeRequestParams(): AuthorizationUrlRequest { @@ -61,15 +58,14 @@ export class EntraIdLoginMiddleware implements NestMiddleware { * The state parameter can also be used to encode information of the app's state before redirect. * You can pass the user's state in the app, such as the page or view they were on, as input to this parameter. */ - const state: string = this.idService.cryptoProvider.base64Encode( - JSON.stringify({ - successRedirect: options.successRedirect || '/', - }) - ) + const state: string = this.idService.encodeObject({ + successRedirect: options.successRedirect || '/', + }) return { state: state, /** + * In future we could use this to set more specific auth scopes for different user types. * By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit: * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes */ diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts index e08c14a1f..f7734486b 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts @@ -1,10 +1,10 @@ import { Controller, Get, Next, Post, Req, Res } from '@nestjs/common'; import { NextFunction, Request, Response } from 'express'; -import { EntraIdLoginMiddleware } from './entra-id-login.middleware'; +import { EntraIdService } from './entra-id.service'; @Controller('auth') export class EntraIdController { - constructor(private readonly entraIdService: EntraIdLoginMiddleware) {} + constructor(private readonly entraIdService: EntraIdService) {} // empty route to trigger the entra-id auth middleware @Get('entra-id') @@ -21,7 +21,7 @@ export class EntraIdController { @Post('entra-redirect') redirectPost(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - console.log('post entra-redirect', req.body) + this.entraIdService.handleAuthRedirect(req, res, next) return '' } } diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts index 471950425..513bc6129 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts @@ -1,4 +1,5 @@ import { + AuthorizationCodeRequest, ClientApplication, ConfidentialClientApplication, Configuration, @@ -7,13 +8,16 @@ import { import { HttpService } from '@nestjs/axios' import { Injectable } from '@nestjs/common' import { AxiosError, AxiosRequestConfig } from 'axios' +import { NextFunction, Request, Response } from 'express' import { firstValueFrom } from 'rxjs' import { catchError } from 'rxjs/operators' import { EntraIdConfigProvider } from './entra-id-config.provider' +import { VerificationData } from './verification-data.interface' @Injectable() export class EntraIdService { - cryptoProvider = new CryptoProvider() + readonly verifierCookieName = 'entra_id_verifier' + private readonly cryptoProvider = new CryptoProvider() constructor( private readonly configProvider: EntraIdConfigProvider, @@ -28,6 +32,60 @@ export class EntraIdService { }) } + async handleAuthRedirect(req: Request, res: Response, next: NextFunction) { + const body = req.body + if (!body.state || !body.code) { + console.error('malformed request body from entra id') + throw 'could not log in' + } + + if (!(this.verifierCookieName in req.cookies)) { + throw 'missing verification data' + } + + try { + const verificationData = this.decodeObject( + req.cookies[this.verifierCookieName] + ) as VerificationData + + verificationData.code = body.code + + const authCodeRequest: AuthorizationCodeRequest = { + scopes: verificationData.scopes, + redirectUri: verificationData.redirectUri, + state: verificationData.state, + codeVerifier: verificationData.codeVerifier, + code: body.code, + } + + const clientApplication = await this.getClientApplication() + + // throws error if verification is false + const tokenResponse = await clientApplication.acquireTokenByCode( + authCodeRequest, + req.body + ) + const decryptedState = this.decodeObject(body.state) + // TODO - do legacy salesforce validation and add these values to token + + res.redirect(this.configProvider.options.successRedirect) + } catch (error) { + next(error) + } + } + + encodeObject(o: object): string { + return this.cryptoProvider.base64Encode(JSON.stringify(o)) + } + + decodeObject(s: string): object { + return JSON.parse(this.cryptoProvider.base64Decode(s)) + } + + async generatePkceCodes() { + return await this.cryptoProvider.generatePkceCodes() + } + private async prepareConfig(): Promise { const config = this.configProvider.msalConfig diff --git a/apps/nestjs-api/src/auth-entra-id/verification-data.interface.ts b/apps/nestjs-api/src/auth-entra-id/verification-data.interface.ts new file mode 100644 index 000000000..087011ceb --- /dev/null +++ b/apps/nestjs-api/src/auth-entra-id/verification-data.interface.ts @@ -0,0 +1,7 @@ +export interface VerificationData { + state: string + scopes: string[] + redirectUri: string + code: string + codeVerifier: string +} \ No newline at end of file diff --git a/apps/nestjs-api/src/main.ts b/apps/nestjs-api/src/main.ts index c75f35216..9543010fa 100644 --- a/apps/nestjs-api/src/main.ts +++ b/apps/nestjs-api/src/main.ts @@ -7,6 +7,7 @@ import { Logger } from '@nestjs/common' import { NestFactory } from '@nestjs/core' import { json, urlencoded } from 'express' import { AppModule } from './app/app.module' +import cookieParser from 'cookie-parser' async function bootstrap() { const app = await NestFactory.create(AppModule) @@ -16,6 +17,7 @@ async function bootstrap() { app.setGlobalPrefix(globalPrefix) app.use(json({ limit: '1mb' })) app.use(urlencoded({ extended: true, limit: '1mb' })) + app.use(cookieParser()) // TODO! Re-enable this? If we can set to a higher debug log level // app.useLogger(SentryService.SentryServiceInstance()) const port = process.env.PORT || 3333 diff --git a/package.json b/package.json index c6089a9e5..accaa132d 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@sentry/node": "^7.43.0", "@sentry/react": "^7.47.0", "@travelerdev/nestjs-sentry-graphql": "^4.1.1", + "cookie-parser": "^1.4.6", "apollo-server-express": "^3.6.7", "@azure/msal-node": "^1.18.4", "async": "^3.2.3", @@ -198,6 +199,7 @@ "@types/react-router": "5.1.1", "@types/react-router-dom": "5.3.3", "@types/yup": "0.26.24", + "@types/cookie-parser": "^1.4.6", "@typescript-eslint/eslint-plugin": "5.10.2", "@typescript-eslint/parser": "5.10.2", "babel-jest": "27.2.3", diff --git a/yarn.lock b/yarn.lock index 81116640b..1d4923088 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4769,6 +4769,13 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.6": + version "1.4.6" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.6.tgz#002643c514cccf883a65cbe044dbdc38c0b92ade" + integrity sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w== + dependencies: + "@types/express" "*" + "@types/cors@2.8.12": version "2.8.12" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" @@ -8149,11 +8156,24 @@ convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, dependencies: safe-buffer "~5.1.1" +cookie-parser@^1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" + integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== + dependencies: + cookie "0.4.1" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= +cookie@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + cookie@0.4.2, cookie@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" From fac1f34c078fadd0cc537f9d605e2d0def5c69f6 Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Sat, 23 Dec 2023 16:02:30 +0100 Subject: [PATCH 11/17] refactor --- .../entra-id-login.middleware.ts | 45 ++++++++++------- .../src/auth-entra-id/entra-id.controller.ts | 7 --- .../src/auth-entra-id/entra-id.service.ts | 49 ++++++++++--------- .../verification-data.interface.ts | 2 +- .../src/pages/front/login/EntraLogin.tsx | 9 ++-- .../src/pages/front/login/Login.scss | 2 +- .../src/pages/front/login/Login.tsx | 3 +- 7 files changed, 63 insertions(+), 54 deletions(-) diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts b/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts index 5fe6db8f9..415879988 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id-login.middleware.ts @@ -1,5 +1,4 @@ import { AuthorizationUrlRequest, ResponseMode } from '@azure/msal-node' - import { Injectable, NestMiddleware } from '@nestjs/common' import { NextFunction, Request, Response } from 'express' import { EntraIdConfigProvider } from './entra-id-config.provider' @@ -23,43 +22,53 @@ export class EntraIdLoginMiddleware implements NestMiddleware { } private async redirectToAuthCodeUrl(res: Response, next: NextFunction) { - // Generate PKCE Codes before starting the authorization flow const { verifier, challenge } = await this.idService.generatePkceCodes() + this.storeVerificationCookie(verifier, res) + + const clientApplication = await this.idService.getClientApplication() + clientApplication + .getAuthCodeUrl(this.prepareAuthCodeUrlRequest(challenge)) + .then((url) => res.redirect(url)) + .catch((err) => { + console.error(err) + next(err) + }) + } + + private storeVerificationCookie(verifier: string, res: Response) { const verificationData = { ...this.authorizationUrlRequestParams, code: '', - codeVerifier: verifier + codeVerifier: verifier, } as VerificationData - res.cookie(this.idService.verifierCookieName, this.idService.encodeObject(verificationData)) + res.cookie( + this.idService.verifierCookieName, + this.idService.encodeObject(verificationData), + { maxAge: 2 * 60 * 60, httpOnly: true } + ) + } - const authCodeUrlRequest = { + private prepareAuthCodeUrlRequest( + challenge: string + ): AuthorizationUrlRequest { + return { ...this.authorizationUrlRequestParams, responseMode: ResponseMode.FORM_POST, // recommended for confidential clients codeChallenge: challenge, codeChallengeMethod: 'S256', } - - const clientApplication = await this.idService.getClientApplication() - clientApplication - .getAuthCodeUrl(authCodeUrlRequest) - .then((url) => res.redirect(url)) - .catch((err) => { - console.error(err) - next(err) - }) } private prepareAuthCodeRequestParams(): AuthorizationUrlRequest { - const options = this.configProvider.options /** * MSAL Node library allows you to pass your custom state as state parameter in the Request object. * The state parameter can also be used to encode information of the app's state before redirect. * You can pass the user's state in the app, such as the page or view they were on, as input to this parameter. */ const state: string = this.idService.encodeObject({ - successRedirect: options.successRedirect || '/', + successRedirect: this.configProvider.options.successRedirect || '/', }) return { @@ -69,8 +78,8 @@ export class EntraIdLoginMiddleware implements NestMiddleware { * By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit: * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes */ - scopes: options.scopes || [], - redirectUri: options.redirectUri, + scopes: this.configProvider.options.scopes || [], + redirectUri: this.configProvider.options.redirectUri, } } } diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts index f7734486b..337ff9963 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.controller.ts @@ -12,15 +12,8 @@ export class EntraIdController { return '' } - @Get('entra-redirect') - redirectGet(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - console.log('get entra-redirect', req.body) - return '' - } - @Post('entra-redirect') redirectPost(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - this.entraIdService.handleAuthRedirect(req, res, next) return '' } diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts index 513bc6129..3d408580b 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts @@ -1,4 +1,5 @@ import { + AuthenticationResult, AuthorizationCodeRequest, ClientApplication, ConfidentialClientApplication, @@ -44,30 +45,13 @@ export class EntraIdService { } try { - const verificationData = this.decodeObject( - req.cookies[this.verifierCookieName] - ) as VerificationData - - verificationData.code = body.code - - const authCodeRequest: AuthorizationCodeRequest = { - scopes: verificationData.scopes, - redirectUri: verificationData.redirectUri, - state: verificationData.state, - codeVerifier: verificationData.codeVerifier, - code: body.code, - } - - const clientApplication = await this.getClientApplication() - - // throws error if verification is false - const tokenResponse = await clientApplication.acquireTokenByCode( - authCodeRequest, - req.body - ) + const tokenResponse = await this.verifyToken(req, res) const decryptedState = this.decodeObject(body.state) - // TODO - do legacy salesforce validation and add these values to token + /** + * TODO - do legacy salesforce validation and add these values to token + * for now, we'll just return to the success redirect page + */ res.redirect(this.configProvider.options.successRedirect) } catch (error) { next(error) @@ -86,6 +70,27 @@ export class EntraIdService { return await this.cryptoProvider.generatePkceCodes() } + private async verifyToken(req: Request, res: Response): Promise { + const verificationData = this.decodeObject( + req.cookies[this.verifierCookieName] + ) as VerificationData + + res.clearCookie(this.verifierCookieName) + + const authCodeRequest: AuthorizationCodeRequest = { + scopes: verificationData.scopes, + redirectUri: verificationData.redirectUri, + state: verificationData.state, + codeVerifier: verificationData.codeVerifier, + code: req.body.code, + } + + const clientApplication = await this.getClientApplication() + + // throws error if verification is false + return clientApplication.acquireTokenByCode(authCodeRequest, req.body) + } + private async prepareConfig(): Promise { const config = this.configProvider.msalConfig diff --git a/apps/nestjs-api/src/auth-entra-id/verification-data.interface.ts b/apps/nestjs-api/src/auth-entra-id/verification-data.interface.ts index 087011ceb..b695280f3 100644 --- a/apps/nestjs-api/src/auth-entra-id/verification-data.interface.ts +++ b/apps/nestjs-api/src/auth-entra-id/verification-data.interface.ts @@ -4,4 +4,4 @@ export interface VerificationData { redirectUri: string code: string codeVerifier: string -} \ No newline at end of file +} diff --git a/apps/redi-connect/src/pages/front/login/EntraLogin.tsx b/apps/redi-connect/src/pages/front/login/EntraLogin.tsx index 8a1fe4bbd..a8aa845ac 100644 --- a/apps/redi-connect/src/pages/front/login/EntraLogin.tsx +++ b/apps/redi-connect/src/pages/front/login/EntraLogin.tsx @@ -1,16 +1,17 @@ import { Heading } from '@talent-connect/shared-atomic-design-components' import { useState } from 'react' -import { Columns } from 'react-bulma-components' +import { Columns, Content } from 'react-bulma-components' import Landing from '../../../components/templates/Landing' export default function EntraLogin() { - const [loginError, setLoginError] = useState('') + const [loginError] = useState('') return ( - - {loginError} + + Entra ID Login - still WIP + {loginError} diff --git a/apps/redi-connect/src/pages/front/login/Login.scss b/apps/redi-connect/src/pages/front/login/Login.scss index 08ad78508..73d24e11a 100644 --- a/apps/redi-connect/src/pages/front/login/Login.scss +++ b/apps/redi-connect/src/pages/front/login/Login.scss @@ -3,4 +3,4 @@ .entra-id-login-button { margin-top: 10px -} \ No newline at end of file +} diff --git a/apps/redi-connect/src/pages/front/login/Login.tsx b/apps/redi-connect/src/pages/front/login/Login.tsx index 8417a8c5f..a9225122e 100644 --- a/apps/redi-connect/src/pages/front/login/Login.tsx +++ b/apps/redi-connect/src/pages/front/login/Login.tsx @@ -81,7 +81,8 @@ export default function Login() { try { history.push('/front/login/entra-redirect') } catch (err) { - setLoginError('Could not log in with Entra') + console.error(err) + setLoginError('Could not log in with microsoft') } } From 5d70aa862a340fba0312d3ff262bb47a2620686d Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Sat, 23 Dec 2023 16:09:40 +0100 Subject: [PATCH 12/17] add feature flag to disable entra id --- apps/redi-connect/src/pages/front/login/Login.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/redi-connect/src/pages/front/login/Login.tsx b/apps/redi-connect/src/pages/front/login/Login.tsx index a9225122e..b0e4da686 100644 --- a/apps/redi-connect/src/pages/front/login/Login.tsx +++ b/apps/redi-connect/src/pages/front/login/Login.tsx @@ -77,6 +77,7 @@ export default function Login() { const [tpProfileLocation, setTpProfileLocation] = useState(null) + const entraIdLoginEnabled = process.env.NX_ENTRA_ID_ENABLED const loginWithEntraId = () => { try { history.push('/front/login/entra-redirect') @@ -313,7 +314,14 @@ export default function Login() { - + {entraIdLoginEnabled && ( + + )} From ae2a5bc637b7399807ab272f31e562f32cb3832a Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Sun, 7 Jan 2024 21:38:12 +0100 Subject: [PATCH 13/17] Update apps/redi-connect/src/pages/front/login/Login.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Anıl Akarsu Signed-off-by: Stuart McLean --- apps/redi-connect/src/pages/front/login/Login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/redi-connect/src/pages/front/login/Login.tsx b/apps/redi-connect/src/pages/front/login/Login.tsx index b0e4da686..c5fdbaa90 100644 --- a/apps/redi-connect/src/pages/front/login/Login.tsx +++ b/apps/redi-connect/src/pages/front/login/Login.tsx @@ -317,7 +317,7 @@ export default function Login() { {entraIdLoginEnabled && ( From d71a5d490c1010e0fe7597b22c27a13b942a1f89 Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Sun, 7 Jan 2024 22:02:02 +0100 Subject: [PATCH 14/17] remove unused files --- .../src/entra-id/entra-id.service.spec.ts | 18 ------------------ .../src/entra-id/entra-id.service.ts | 4 ---- 2 files changed, 22 deletions(-) delete mode 100644 apps/nestjs-api/src/entra-id/entra-id.service.spec.ts delete mode 100644 apps/nestjs-api/src/entra-id/entra-id.service.ts diff --git a/apps/nestjs-api/src/entra-id/entra-id.service.spec.ts b/apps/nestjs-api/src/entra-id/entra-id.service.spec.ts deleted file mode 100644 index 265098445..000000000 --- a/apps/nestjs-api/src/entra-id/entra-id.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { EntraIdService } from './entra-id.service'; - -describe('EntraIdService', () => { - let service: EntraIdService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [EntraIdService], - }).compile(); - - service = module.get(EntraIdService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/nestjs-api/src/entra-id/entra-id.service.ts b/apps/nestjs-api/src/entra-id/entra-id.service.ts deleted file mode 100644 index 57f666cd6..000000000 --- a/apps/nestjs-api/src/entra-id/entra-id.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class EntraIdService {} From 3e25cc6fdfac7db2a7e51d202e14399800c873b3 Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Sun, 7 Jan 2024 22:03:29 +0100 Subject: [PATCH 15/17] simplify service instantiator --- apps/nestjs-api/src/auth-entra-id/entra-id.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts index 3d408580b..552408822 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id.service.ts @@ -28,9 +28,7 @@ export class EntraIdService { async getClientApplication(): Promise { const config = await this.prepareConfig() - return new Promise((resolve) => { - return resolve(new ConfidentialClientApplication(config)) - }) + return new ConfidentialClientApplication(config) } async handleAuthRedirect(req: Request, res: Response, next: NextFunction) { From eae75ee57a7809ae7d2b03b57bd9dfd4fb440204 Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Sun, 7 Jan 2024 22:04:04 +0100 Subject: [PATCH 16/17] centralize nest api url and usage thereof --- apps/redi-connect/src/routes/routes__logged-out.tsx | 4 ++-- apps/redi-connect/src/services/api/api.tsx | 6 +----- libs/data-access/src/lib/graphql-client.ts | 8 ++------ libs/shared-config/src/lib/config.ts | 6 +++--- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/apps/redi-connect/src/routes/routes__logged-out.tsx b/apps/redi-connect/src/routes/routes__logged-out.tsx index 773e635a4..4aac20a00 100644 --- a/apps/redi-connect/src/routes/routes__logged-out.tsx +++ b/apps/redi-connect/src/routes/routes__logged-out.tsx @@ -1,3 +1,4 @@ +import { NEST_API_URL } from '@talent-connect/shared-config' import { lazy } from 'react' import Faqs from '../pages/front/Faqs' import Home from '../pages/front/landing/Home' @@ -5,7 +6,6 @@ import Mentee from '../pages/front/landing/Mentee' import Mentor from '../pages/front/landing/Mentor' import { RequestResetPasswordEmail } from '../pages/front/reset-password/RequestResetPasswordEmail' import { SetNewPassword } from '../pages/front/reset-password/SetNewPassword' -import { entraLoginUrl } from '../services/api/api' import { RouteDefinition } from './index' const Login = lazy( () => @@ -84,7 +84,7 @@ export const routes__loggedOut: RouteDefinition[] = [ { path: '/front/login/entra-redirect', component: () => { - window.location.href = entraLoginUrl() + window.location.href = `${NEST_API_URL}/auth/entra-id` return null }, exact: true, diff --git a/apps/redi-connect/src/services/api/api.tsx b/apps/redi-connect/src/services/api/api.tsx index 78db6f9a4..ef5bc588d 100644 --- a/apps/redi-connect/src/services/api/api.tsx +++ b/apps/redi-connect/src/services/api/api.tsx @@ -1,4 +1,4 @@ -import { API_URL, NEST_API_URL } from '@talent-connect/shared-config' +import { API_URL } from '@talent-connect/shared-config' import { AccessToken } from '@talent-connect/shared-types' import axios from 'axios' import { @@ -51,10 +51,6 @@ export const logout = () => { history.push('/front/home') } -export const entraLoginUrl = (): string => { - return `${NEST_API_URL}/auth/entra-id` -} - export const requestResetPasswordEmail = async (email: string) => { email = email.toLowerCase() await axios(`${API_URL}/redUsers/requestResetPasswordEmail`, { diff --git a/libs/data-access/src/lib/graphql-client.ts b/libs/data-access/src/lib/graphql-client.ts index ad5f5da16..62c0df44e 100644 --- a/libs/data-access/src/lib/graphql-client.ts +++ b/libs/data-access/src/lib/graphql-client.ts @@ -1,11 +1,7 @@ +import { NEST_API_URL } from '@talent-connect/shared-config' import { GraphQLClient } from 'graphql-request' -const endpoint = - process.env.NODE_ENV === 'production' - ? 'https://connect-nestjs-api.redi-school.org/graphql' - : 'http://localhost:3333/graphql' - -export const graphqlClient = new GraphQLClient(endpoint, { +export const graphqlClient = new GraphQLClient(NEST_API_URL, { // headers: { // authorization: 'Bearer MY_TOKEN', // }, diff --git a/libs/shared-config/src/lib/config.ts b/libs/shared-config/src/lib/config.ts index 054fb98c6..290874a5a 100644 --- a/libs/shared-config/src/lib/config.ts +++ b/libs/shared-config/src/lib/config.ts @@ -368,9 +368,9 @@ export const MENTORSHIP_MATCH_STATUS_LABELS: any = { INVALIDATED_AS_OTHER_MENTOR_ACCEPTED: 'Cancelled', } -export const NEST_API_URL = process.env.NX_NEST_API_URL - ? process.env.NX_NEST_API_URL - : 'http://localhost:3333/api' +export const NEST_API_URL = process.env.NODE_ENV === 'production' + ? 'https://connect-nestjs-api.redi-school.org/graphql' + : 'http://localhost:3333/graphql' export const API_URL = process.env.NX_API_URL ? process.env.NX_API_URL : 'http://localhost:3003/api' From c485208dd311f81201bf207922ccbfe92fea9b67 Mon Sep 17 00:00:00 2001 From: Stuart McLean Date: Sun, 7 Jan 2024 22:42:07 +0100 Subject: [PATCH 17/17] repair nest api url usage --- apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts | 3 ++- apps/redi-connect/src/routes/routes__logged-out.tsx | 2 +- libs/data-access/src/lib/graphql-client.ts | 2 +- libs/shared-config/src/lib/config.ts | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts b/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts index c7892967e..2f5d8a589 100644 --- a/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts +++ b/apps/nestjs-api/src/auth-entra-id/entra-id-config.provider.ts @@ -1,6 +1,7 @@ import msal, { LogLevel } from '@azure/msal-node' import { Injectable } from '@nestjs/common' import { ConfigService } from '@nestjs/config' +import { NEST_API_URL } from '@talent-connect/shared-config' import { EntraIdLoginOptions } from './entra-id-login-options.interface' @Injectable() @@ -11,7 +12,7 @@ export class EntraIdConfigProvider { constructor(configService: ConfigService) { this.options = { scopes: [], - redirectUri: configService.get('NX_NESTJS_API_URI') + '/auth/entra-redirect', // to backend + redirectUri: `${NEST_API_URL}/api/auth/entra-redirect`, // to backend successRedirect: configService.get('NX_FRONTEND_URI') + '/front/login/entra-login', // to frontend cloudInstance: configService.get('NX_ENTRA_ID_CLOUD_INSTANCE'), } diff --git a/apps/redi-connect/src/routes/routes__logged-out.tsx b/apps/redi-connect/src/routes/routes__logged-out.tsx index 4aac20a00..6bc85f2bb 100644 --- a/apps/redi-connect/src/routes/routes__logged-out.tsx +++ b/apps/redi-connect/src/routes/routes__logged-out.tsx @@ -84,7 +84,7 @@ export const routes__loggedOut: RouteDefinition[] = [ { path: '/front/login/entra-redirect', component: () => { - window.location.href = `${NEST_API_URL}/auth/entra-id` + window.location.href = `${NEST_API_URL}/api/auth/entra-id` return null }, exact: true, diff --git a/libs/data-access/src/lib/graphql-client.ts b/libs/data-access/src/lib/graphql-client.ts index 62c0df44e..69f486216 100644 --- a/libs/data-access/src/lib/graphql-client.ts +++ b/libs/data-access/src/lib/graphql-client.ts @@ -1,7 +1,7 @@ import { NEST_API_URL } from '@talent-connect/shared-config' import { GraphQLClient } from 'graphql-request' -export const graphqlClient = new GraphQLClient(NEST_API_URL, { +export const graphqlClient = new GraphQLClient(`${NEST_API_URL}/graphql`, { // headers: { // authorization: 'Bearer MY_TOKEN', // }, diff --git a/libs/shared-config/src/lib/config.ts b/libs/shared-config/src/lib/config.ts index 290874a5a..393aaf28f 100644 --- a/libs/shared-config/src/lib/config.ts +++ b/libs/shared-config/src/lib/config.ts @@ -369,8 +369,8 @@ export const MENTORSHIP_MATCH_STATUS_LABELS: any = { } export const NEST_API_URL = process.env.NODE_ENV === 'production' - ? 'https://connect-nestjs-api.redi-school.org/graphql' - : 'http://localhost:3333/graphql' + ? 'https://connect-nestjs-api.redi-school.org' + : 'http://localhost:3333' export const API_URL = process.env.NX_API_URL ? process.env.NX_API_URL : 'http://localhost:3003/api'