prime-sdk 3.0.4 → 3.0.7-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.eslintrc.js +4 -0
  2. package/.yarn/install-state.gz +0 -0
  3. package/.yarnrc.yml +1 -0
  4. package/README.md +307 -0
  5. package/dist/external/aws/s3/s3.provider.d.ts +1 -0
  6. package/dist/external/aws/s3/s3.provider.js +5 -0
  7. package/dist/external/aws/s3/s3.provider.js.map +1 -1
  8. package/dist/external/aws/sns/sns.provider.d.ts +1 -0
  9. package/dist/external/aws/sns/sns.provider.js +5 -0
  10. package/dist/external/aws/sns/sns.provider.js.map +1 -1
  11. package/dist/external/aws/sqs/sqs.provider.d.ts +1 -0
  12. package/dist/external/aws/sqs/sqs.provider.js +5 -0
  13. package/dist/external/aws/sqs/sqs.provider.js.map +1 -1
  14. package/dist/index.d.ts +15 -1
  15. package/dist/index.js +25 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib/entraID/customError/entraID.exception.d.ts +5 -0
  18. package/dist/lib/entraID/customError/entraID.exception.js +43 -0
  19. package/dist/lib/entraID/customError/entraID.exception.js.map +1 -0
  20. package/dist/lib/entraID/decorators/current-user.decorator.d.ts +1 -0
  21. package/dist/lib/entraID/decorators/current-user.decorator.js +9 -0
  22. package/dist/lib/entraID/decorators/current-user.decorator.js.map +1 -0
  23. package/dist/lib/entraID/decorators/public.decorator.d.ts +2 -0
  24. package/dist/lib/entraID/decorators/public.decorator.js +8 -0
  25. package/dist/lib/entraID/decorators/public.decorator.js.map +1 -0
  26. package/dist/lib/entraID/decorators/roles.decorator.d.ts +2 -0
  27. package/dist/lib/entraID/decorators/roles.decorator.js +8 -0
  28. package/dist/lib/entraID/decorators/roles.decorator.js.map +1 -0
  29. package/dist/lib/entraID/entraID.constants.d.ts +2 -0
  30. package/dist/lib/entraID/entraID.constants.js +6 -0
  31. package/dist/lib/entraID/entraID.constants.js.map +1 -0
  32. package/dist/lib/entraID/entraID.module.d.ts +14 -0
  33. package/dist/lib/entraID/entraID.module.js +56 -0
  34. package/dist/lib/entraID/entraID.module.js.map +1 -0
  35. package/dist/lib/entraID/entraID.service.d.ts +25 -0
  36. package/dist/lib/entraID/entraID.service.js +166 -0
  37. package/dist/lib/entraID/entraID.service.js.map +1 -0
  38. package/dist/lib/entraID/guards/entra-auth.guard.d.ts +21 -0
  39. package/dist/lib/entraID/guards/entra-auth.guard.js +70 -0
  40. package/dist/lib/entraID/guards/entra-auth.guard.js.map +1 -0
  41. package/dist/lib/entraID/guards/roles.guard.d.ts +7 -0
  42. package/dist/lib/entraID/guards/roles.guard.js +41 -0
  43. package/dist/lib/entraID/guards/roles.guard.js.map +1 -0
  44. package/dist/lib/entraID/interfaces/entraID-service.interface.d.ts +7 -0
  45. package/dist/lib/entraID/interfaces/entraID-service.interface.js +3 -0
  46. package/dist/lib/entraID/interfaces/entraID-service.interface.js.map +1 -0
  47. package/dist/lib/entraID/internal-token/internal-token.module.d.ts +2 -0
  48. package/dist/lib/entraID/internal-token/internal-token.module.js +21 -0
  49. package/dist/lib/entraID/internal-token/internal-token.module.js.map +1 -0
  50. package/dist/lib/entraID/internal-token/internal-token.service.d.ts +10 -0
  51. package/dist/lib/entraID/internal-token/internal-token.service.js +76 -0
  52. package/dist/lib/entraID/internal-token/internal-token.service.js.map +1 -0
  53. package/dist/lib/entraID/microsoft/microsoft.module.d.ts +2 -0
  54. package/dist/lib/entraID/microsoft/microsoft.module.js +21 -0
  55. package/dist/lib/entraID/microsoft/microsoft.module.js.map +1 -0
  56. package/dist/lib/entraID/microsoft/microsoft.service.d.ts +14 -0
  57. package/dist/lib/entraID/microsoft/microsoft.service.js +132 -0
  58. package/dist/lib/entraID/microsoft/microsoft.service.js.map +1 -0
  59. package/dist/lib/entraID/security/security.module.d.ts +2 -0
  60. package/dist/lib/entraID/security/security.module.js +21 -0
  61. package/dist/lib/entraID/security/security.module.js.map +1 -0
  62. package/dist/lib/entraID/security/security.service.d.ts +9 -0
  63. package/dist/lib/entraID/security/security.service.js +65 -0
  64. package/dist/lib/entraID/security/security.service.js.map +1 -0
  65. package/dist/lib/entraID/types/entraID-module-init.types.d.ts +18 -0
  66. package/dist/lib/entraID/types/entraID-module-init.types.js +3 -0
  67. package/dist/lib/entraID/types/entraID-module-init.types.js.map +1 -0
  68. package/dist/lib/entraID/types/entraID.type.d.ts +52 -0
  69. package/dist/lib/entraID/types/entraID.type.js +3 -0
  70. package/dist/lib/entraID/types/entraID.type.js.map +1 -0
  71. package/dist/lib/entraID/utils/cookie.utils.d.ts +7 -0
  72. package/dist/lib/entraID/utils/cookie.utils.js +42 -0
  73. package/dist/lib/entraID/utils/cookie.utils.js.map +1 -0
  74. package/dist/lib/entraID/utils/crypto.utils.d.ts +9 -0
  75. package/dist/lib/entraID/utils/crypto.utils.js +47 -0
  76. package/dist/lib/entraID/utils/crypto.utils.js.map +1 -0
  77. package/dist/lib/entraID/utils/index.d.ts +2 -0
  78. package/dist/lib/entraID/utils/index.js +8 -0
  79. package/dist/lib/entraID/utils/index.js.map +1 -0
  80. package/dist/lib/entraID/utils/token.utils.d.ts +6 -0
  81. package/dist/lib/entraID/utils/token.utils.js +24 -0
  82. package/dist/lib/entraID/utils/token.utils.js.map +1 -0
  83. package/dist/tsconfig.build.tsbuildinfo +1 -1
  84. package/package.json +5 -1
  85. package/src/external/aws/s3/s3.provider.ts +6 -0
  86. package/src/external/aws/sns/sns.provider.ts +6 -0
  87. package/src/external/aws/sqs/sqs.provider.ts +6 -0
  88. package/src/index.ts +42 -0
  89. package/src/lib/entraID/customError/entraID.exception.ts +43 -0
  90. package/src/lib/entraID/decorators/current-user.decorator.ts +8 -0
  91. package/src/lib/entraID/decorators/public.decorator.ts +5 -0
  92. package/src/lib/entraID/decorators/roles.decorator.ts +5 -0
  93. package/src/lib/entraID/entraID.constants.ts +2 -0
  94. package/src/lib/entraID/entraID.module.ts +78 -0
  95. package/src/lib/entraID/entraID.service.ts +206 -0
  96. package/src/lib/entraID/guards/entra-auth.guard.ts +77 -0
  97. package/src/lib/entraID/guards/roles.guard.ts +34 -0
  98. package/src/lib/entraID/interfaces/entraID-service.interface.ts +12 -0
  99. package/src/lib/entraID/internal-token/internal-token.module.ts +8 -0
  100. package/src/lib/entraID/internal-token/internal-token.service.ts +66 -0
  101. package/src/lib/entraID/microsoft/microsoft.module.ts +8 -0
  102. package/src/lib/entraID/microsoft/microsoft.service.ts +158 -0
  103. package/src/lib/entraID/security/security.module.ts +8 -0
  104. package/src/lib/entraID/security/security.service.ts +68 -0
  105. package/src/lib/entraID/types/entraID-module-init.types.ts +25 -0
  106. package/src/lib/entraID/types/entraID.type.ts +57 -0
  107. package/src/lib/entraID/utils/cookie.utils.ts +50 -0
  108. package/src/lib/entraID/utils/crypto.utils.ts +53 -0
  109. package/src/lib/entraID/utils/index.ts +2 -0
  110. package/src/lib/entraID/utils/token.utils.ts +20 -0
  111. package/tsconfig.json +1 -0
  112. package/.vscode/settings.json +0 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prime-sdk",
3
- "version": "3.0.4",
3
+ "version": "3.0.7-beta.0",
4
4
  "description": "swiss army knife",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -35,7 +35,10 @@
35
35
  "@nestjs/config": "^4.0.3",
36
36
  "@nestjs/core": "^11.1.17",
37
37
  "@nestjs/platform-express": "^11.1.17",
38
+ "@smithy/node-http-handler": "^3.3.1",
38
39
  "ioredis": "^5.4.1",
40
+ "jsonwebtoken": "^9.0.3",
41
+ "jwks-rsa": "^4.0.1",
39
42
  "reflect-metadata": "^0.2.0",
40
43
  "rxjs": "^7.8.1"
41
44
  },
@@ -49,6 +52,7 @@
49
52
  "@nestjs/testing": "^11.1.17",
50
53
  "@types/express": "^4.17.17",
51
54
  "@types/jest": "^29.5.2",
55
+ "@types/jsonwebtoken": "^9.0.10",
52
56
  "@types/node": "^20.3.1",
53
57
  "@types/supertest": "^6.0.0",
54
58
  "@typescript-eslint/eslint-plugin": "^6.0.0",
@@ -1,5 +1,7 @@
1
1
  import { S3Client } from '@aws-sdk/client-s3';
2
+ import { NodeHttpHandler } from '@smithy/node-http-handler';
2
3
  import { DynamicModule } from '@nestjs/common';
4
+ import { Agent as HttpsAgent } from 'https';
3
5
  import {
4
6
  AwsCredentialIdentity,
5
7
  AwsCredentialIdentityProvider,
@@ -9,6 +11,7 @@ export type S3Setup = {
9
11
  region: string;
10
12
  endpoint?: string;
11
13
  credentials: AwsCredentialIdentityProvider | AwsCredentialIdentity;
14
+ keepAlive?: boolean;
12
15
  };
13
16
 
14
17
  const S3Provider = (params: S3Setup) => {
@@ -19,6 +22,9 @@ const S3Provider = (params: S3Setup) => {
19
22
  credentials: params.credentials,
20
23
  ...(params.endpoint && { endpoint: params.endpoint }),
21
24
  region: params.region,
25
+ requestHandler: new NodeHttpHandler({
26
+ httpsAgent: new HttpsAgent({ keepAlive: params.keepAlive ?? true }),
27
+ }),
22
28
  });
23
29
  },
24
30
  };
@@ -1,5 +1,7 @@
1
1
  import { SNSClient } from '@aws-sdk/client-sns';
2
+ import { NodeHttpHandler } from '@smithy/node-http-handler';
2
3
  import { DynamicModule } from '@nestjs/common';
4
+ import { Agent as HttpsAgent } from 'https';
3
5
  import {
4
6
  AwsCredentialIdentity,
5
7
  AwsCredentialIdentityProvider,
@@ -9,6 +11,7 @@ export type SNSSetup = {
9
11
  region: string;
10
12
  endpoint?: string;
11
13
  credentials: AwsCredentialIdentityProvider | AwsCredentialIdentity;
14
+ keepAlive?: boolean;
12
15
  };
13
16
 
14
17
  const SNSProvider = (params: SNSSetup) => {
@@ -19,6 +22,9 @@ const SNSProvider = (params: SNSSetup) => {
19
22
  credentials: params.credentials,
20
23
  ...(params.endpoint && { endpoint: params.endpoint }),
21
24
  region: params.region,
25
+ requestHandler: new NodeHttpHandler({
26
+ httpsAgent: new HttpsAgent({ keepAlive: params.keepAlive ?? true }),
27
+ }),
22
28
  });
23
29
  },
24
30
  };
@@ -1,5 +1,7 @@
1
1
  import { SQSClient } from '@aws-sdk/client-sqs';
2
+ import { NodeHttpHandler } from '@smithy/node-http-handler';
2
3
  import { DynamicModule } from '@nestjs/common';
4
+ import { Agent as HttpsAgent } from 'https';
3
5
  import {
4
6
  AwsCredentialIdentity,
5
7
  AwsCredentialIdentityProvider,
@@ -9,6 +11,7 @@ export type SqsSetup = {
9
11
  region: string;
10
12
  endpoint?: string;
11
13
  credentials: AwsCredentialIdentityProvider | AwsCredentialIdentity;
14
+ keepAlive?: boolean;
12
15
  };
13
16
 
14
17
  const SqsProvider = (params: SqsSetup) => {
@@ -19,6 +22,9 @@ const SqsProvider = (params: SqsSetup) => {
19
22
  credentials: params.credentials,
20
23
  region: params.region,
21
24
  ...(params.endpoint && { endpoint: params.endpoint }),
25
+ requestHandler: new NodeHttpHandler({
26
+ httpsAgent: new HttpsAgent({ keepAlive: params.keepAlive ?? true }),
27
+ }),
22
28
  });
23
29
  },
24
30
  };
package/src/index.ts CHANGED
@@ -33,6 +33,28 @@ import {
33
33
  TypeRedis,
34
34
  PubSubRedis,
35
35
  } from './external/redis/types/ioredis.types';
36
+ import { EntraIDModule, ENTRA_ID_CONFIG } from './lib/entraID/entraID.module';
37
+ import type { EntraIDModuleOptions } from './lib/entraID/entraID.module';
38
+ import { EntraIDService } from './lib/entraID/entraID.service';
39
+ import { EntraAuthGuard } from './lib/entraID/guards/entra-auth.guard';
40
+ import { RolesGuard } from './lib/entraID/guards/roles.guard';
41
+ import { InternalTokenService } from './lib/entraID/internal-token/internal-token.service';
42
+ import { CurrentUser } from './lib/entraID/decorators/current-user.decorator';
43
+ import { Public } from './lib/entraID/decorators/public.decorator';
44
+ import { Roles } from './lib/entraID/decorators/roles.decorator';
45
+ import { CookieUtils, TokenUtils } from './lib/entraID/utils';
46
+ import { SecurityService } from './lib/entraID/security/security.service';
47
+ import { EntraIDException } from './lib/entraID/customError/entraID.exception';
48
+ import type {
49
+ EntraUser,
50
+ AuthResult,
51
+ CallbackParams,
52
+ TokenEnrichmentContext,
53
+ } from './lib/entraID/types/entraID.type';
54
+ import type {
55
+ EntraIDSetup,
56
+ EntraIDAsyncOptions,
57
+ } from './lib/entraID/types/entraID-module-init.types';
36
58
 
37
59
  export {
38
60
  QueueModule,
@@ -61,4 +83,24 @@ export {
61
83
  ClusterRedisSetup,
62
84
  TypeRedis,
63
85
  PubSubRedis,
86
+ EntraIDModule,
87
+ EntraIDModuleOptions,
88
+ ENTRA_ID_CONFIG,
89
+ EntraIDService,
90
+ EntraAuthGuard,
91
+ RolesGuard,
92
+ InternalTokenService,
93
+ CurrentUser,
94
+ Public,
95
+ Roles,
96
+ CookieUtils,
97
+ TokenUtils,
98
+ SecurityService,
99
+ EntraIDException,
100
+ EntraIDSetup,
101
+ EntraIDAsyncOptions,
102
+ EntraUser,
103
+ AuthResult,
104
+ CallbackParams,
105
+ TokenEnrichmentContext,
64
106
  };
@@ -0,0 +1,43 @@
1
+ const entraIDErrorMessages: Record<string, string> = {
2
+ INVALID_CONFIG: 'Configuração inválida do módulo EntraID',
3
+ TENANT_NOT_FOUND: 'Tenant não encontrado na configuração do EntraID',
4
+ INVALID_SECRET: 'Em Produção é necessário um secret válido',
5
+ STATE_MISMATCH: 'State OAuth inválido ou expirado',
6
+ TOKEN_INVALID_HEADER: 'Header da chave publica inválido',
7
+ PAYLOAD_ID_TOKEN_INVALID: 'Payload decodificado é inválido',
8
+ INVALID_NONCE: 'Nonce retornado é diferente do esperado',
9
+ CODE_EXCHANGE_FAILED: 'Falha ao trocar o código de autorização pelo token',
10
+ TOKEN_EXPIRED: 'Token de acesso expirado',
11
+ TOKEN_INVALID: 'Token de acesso inválido',
12
+ RESERVED_CLAIM_OVERRIDE:
13
+ 'enrichTokenPayload tentou sobrescrever um claim reservado (sub/iss/aud/exp/iat/nbf/jti/groups)',
14
+ TOKEN_AUDIENCE_INVALID: 'Audience do token não corresponde ao esperado',
15
+ JWKS_FETCH_FAILED: 'Falha ao buscar chaves públicas do Azure',
16
+ JWKS_KEY_NOT_FOUND: 'Chave pública não encontrada no JWKS',
17
+ GRAPH_FETCH_FAILED: 'Falha ao buscar dados do usuário no Microsoft Graph',
18
+ GRAPH_USER_NOT_FOUND: 'Usuário não encontrado no Microsoft Entra ID',
19
+ SECURITY_PATH_TRAVERSAL: 'Tentativa de path traversal detectada',
20
+ SECURITY_NULL_BYTE: 'Caractere nulo detectado na entrada',
21
+ SECURITY_INVALID_CHARS: 'Caracteres inválidos ou perigosos na entrada',
22
+ SECURITY_OPEN_REDIRECT: 'Redirecionamento para URL não autorizada',
23
+ SECURITY_INVALID_URL: 'URL inválida',
24
+ OAUTH_PROVIDER_ERROR: 'Erro retornado pelo provedor OAuth',
25
+ OAUTH_MISSING_PARAMS: 'Parâmetros obrigatórios ausentes no callback OAuth',
26
+ REFRESH_TOKEN_FAILED: 'Falha ao renovar o token de acesso',
27
+ REVOKE_SESSIONS_FAILED:
28
+ 'Falha ao revogar sessões do usuário no Microsoft Entra ID',
29
+ UNKNOWN_ERROR: 'Erro desconhecido',
30
+ };
31
+
32
+ export class EntraIDException extends Error {
33
+ public readonly message: string;
34
+
35
+ constructor(public readonly code: string) {
36
+ const message =
37
+ entraIDErrorMessages[code] ?? entraIDErrorMessages['UNKNOWN_ERROR'];
38
+ super(message);
39
+ this.message = message;
40
+ this.name = 'EntraIDException';
41
+ Object.setPrototypeOf(this, EntraIDException.prototype);
42
+ }
43
+ }
@@ -0,0 +1,8 @@
1
+ import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2
+
3
+ export const CurrentUser = createParamDecorator(
4
+ <T = Record<string, unknown>>(_data: unknown, ctx: ExecutionContext): T => {
5
+ const request = ctx.switchToHttp().getRequest();
6
+ return request.user as T;
7
+ },
8
+ );
@@ -0,0 +1,5 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+
3
+ export const IS_PUBLIC_KEY = 'isPublic';
4
+
5
+ export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@@ -0,0 +1,5 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+
3
+ export const ROLES_KEY = 'roles';
4
+
5
+ export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
@@ -0,0 +1,2 @@
1
+ export const ENTRA_ID_CONFIG = 'ENTRA_ID_CONFIG';
2
+ export const MICROSOFT_AUTHORITY_URL = 'https://login.microsoftonline.com';
@@ -0,0 +1,78 @@
1
+ import {
2
+ DynamicModule,
3
+ Module,
4
+ ModuleMetadata,
5
+ Provider,
6
+ Type,
7
+ } from '@nestjs/common';
8
+ import { EntraIDService } from './entraID.service';
9
+ import { EntraAuthGuard } from './guards/entra-auth.guard';
10
+ import { InternalTokenService } from './internal-token/internal-token.service';
11
+ import { MicrosoftModule } from './microsoft/microsoft.module';
12
+ import { SecurityModule } from './security/security.module';
13
+ import { ENTRA_ID_CONFIG } from './entraID.constants';
14
+ import {
15
+ EntraIDAsyncOptions,
16
+ EntraIDSetup,
17
+ } from './types/entraID-module-init.types';
18
+
19
+ export { ENTRA_ID_CONFIG };
20
+
21
+ export type EntraIDModuleOptions = {
22
+ internalTokenService?: Type<InternalTokenService>;
23
+ imports?: ModuleMetadata['imports'];
24
+ };
25
+
26
+ @Module({})
27
+ export class EntraIDModule {
28
+ private static buildModule(
29
+ configProvider: Provider,
30
+ imports: ModuleMetadata['imports'] = [],
31
+ options: EntraIDModuleOptions = {},
32
+ ): DynamicModule {
33
+ const tokenServiceClass =
34
+ options.internalTokenService ?? InternalTokenService;
35
+
36
+ return {
37
+ global: true,
38
+ module: EntraIDModule,
39
+ imports: [...imports, ...(options.imports ?? []), MicrosoftModule, SecurityModule],
40
+ providers: [
41
+ configProvider,
42
+ tokenServiceClass,
43
+ { provide: InternalTokenService, useExisting: tokenServiceClass },
44
+ EntraIDService,
45
+ EntraAuthGuard,
46
+ ],
47
+ exports: [
48
+ ENTRA_ID_CONFIG,
49
+ InternalTokenService,
50
+ EntraIDService,
51
+ EntraAuthGuard,
52
+ ],
53
+ };
54
+ }
55
+
56
+ static forRoot(params: EntraIDSetup, options?: EntraIDModuleOptions): DynamicModule {
57
+ return this.buildModule(
58
+ { provide: ENTRA_ID_CONFIG, useValue: params },
59
+ [],
60
+ options,
61
+ );
62
+ }
63
+
64
+ static forRootAsync(
65
+ paramsAsync: EntraIDAsyncOptions,
66
+ options?: EntraIDModuleOptions,
67
+ ): DynamicModule {
68
+ return this.buildModule(
69
+ {
70
+ provide: ENTRA_ID_CONFIG,
71
+ useFactory: paramsAsync.useFactory,
72
+ inject: paramsAsync.inject ?? [],
73
+ },
74
+ paramsAsync.imports ?? [],
75
+ options,
76
+ );
77
+ }
78
+ }
@@ -0,0 +1,206 @@
1
+ import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
2
+ import jwt, { Jwt } from 'jsonwebtoken';
3
+ import jwksClient from 'jwks-rsa';
4
+ import { ENTRA_ID_CONFIG, MICROSOFT_AUTHORITY_URL } from './entraID.constants';
5
+ import { InternalTokenService } from './internal-token/internal-token.service';
6
+ import { MicrosoftService } from './microsoft/microsoft.service';
7
+ import { SecurityService } from './security/security.service';
8
+ import {
9
+ AuthResult,
10
+ CallbackParams,
11
+ CreateStateNonce,
12
+ EntraUser,
13
+ IdTokenPayload,
14
+ UrlRedirectSso,
15
+ } from './types/entraID.type';
16
+ import { EntraIDException } from './customError/entraID.exception';
17
+ import { EntraIDSetup } from './types/entraID-module-init.types';
18
+ import { CryptoUtils } from './utils/crypto.utils';
19
+ import { IEntraIDService } from './interfaces/entraID-service.interface';
20
+
21
+ const STATE_TTL_MS = 600_000;
22
+
23
+ @Injectable()
24
+ export class EntraIDService implements OnModuleInit, IEntraIDService {
25
+ private jwksClientRsa: ReturnType<typeof jwksClient>;
26
+
27
+ constructor(
28
+ @Inject(ENTRA_ID_CONFIG) protected readonly config: EntraIDSetup,
29
+ protected readonly microsoftService: MicrosoftService,
30
+ protected readonly securityService: SecurityService,
31
+ protected readonly internalTokenService: InternalTokenService,
32
+ ) {}
33
+
34
+ onModuleInit() {
35
+ const { tenantID, clientID, clientSecret, stateSecret, redirectUri } =
36
+ this.config;
37
+
38
+ if (
39
+ !tenantID ||
40
+ !clientID ||
41
+ !clientSecret ||
42
+ !stateSecret ||
43
+ !redirectUri
44
+ ) {
45
+ throw new EntraIDException('INVALID_CONFIG');
46
+ }
47
+
48
+ const authorityUrl = this.config.authorityUrl ?? MICROSOFT_AUTHORITY_URL;
49
+
50
+ this.jwksClientRsa = jwksClient({
51
+ jwksUri: `${authorityUrl}/${tenantID}/discovery/v2.0/keys`,
52
+ cache: true,
53
+ cacheMaxAge: 10 * 60 * 1000,
54
+ });
55
+ }
56
+
57
+ async handleCallback(params: CallbackParams): Promise<AuthResult> {
58
+ this.securityService.validateCallbackParams(params);
59
+
60
+ const nonce = this.verifyStateAndReturnNonce(params.state!);
61
+ const tokens = await this.microsoftService.exchangeCode(params.code!);
62
+ const payload = await this.verifyIdToken(tokens.id_token, nonce);
63
+
64
+ const email = payload.preferred_username ?? payload.email ?? payload.upn;
65
+ if (!email) {
66
+ throw new EntraIDException('PAYLOAD_ID_TOKEN_INVALID');
67
+ }
68
+
69
+ const result = await this.buildUserJwt(
70
+ tokens.access_token,
71
+ email,
72
+ payload.name,
73
+ );
74
+ return { ...result, refreshToken: tokens.refresh_token };
75
+ }
76
+
77
+ async refresh(refreshToken: string): Promise<AuthResult> {
78
+ const tokens = await this.microsoftService.refreshToken(refreshToken);
79
+ const result = await this.buildUserJwt(tokens.access_token, '', '');
80
+ return { ...result, refreshToken: tokens.refresh_token };
81
+ }
82
+
83
+ async logout(userId: string): Promise<void> {
84
+ await this.microsoftService.revokeUserSessions(userId);
85
+ }
86
+
87
+ async initiateLogin(): Promise<UrlRedirectSso> {
88
+ const { state, nonce } = this.createSignedState();
89
+ const { clientID, redirectUri, tenantID, scope, prompt } = this.config;
90
+ const authorityUrl = this.config.authorityUrl ?? MICROSOFT_AUTHORITY_URL;
91
+
92
+ const params = new URLSearchParams({
93
+ client_id: clientID,
94
+ response_type: 'code',
95
+ redirect_uri: redirectUri,
96
+ response_mode: 'form_post',
97
+ scope: scope ?? 'openid profile email User.Read GroupMember.Read.All',
98
+ state,
99
+ nonce,
100
+ prompt: prompt ?? 'select_account',
101
+ });
102
+
103
+ return `${authorityUrl}/${tenantID}/oauth2/v2.0/authorize?${params.toString()}`;
104
+ }
105
+
106
+ decodeLocalToken(token: string): EntraUser {
107
+ return this.internalTokenService.verify(token);
108
+ }
109
+
110
+ protected async verifyIdToken(
111
+ idToken: string,
112
+ nonce: string,
113
+ ): Promise<IdTokenPayload> {
114
+ const decoded = jwt.decode(idToken, { complete: true }) as Jwt | null;
115
+
116
+ if (!decoded?.header) {
117
+ throw new EntraIDException('TOKEN_INVALID_HEADER');
118
+ }
119
+
120
+ const publicKey = await CryptoUtils.getPublicKeyRsa(
121
+ decoded.header.kid,
122
+ this.jwksClientRsa,
123
+ );
124
+
125
+ const expectedIssuers: [string, ...string[]] = [
126
+ `https://login.microsoftonline.com/${this.config.tenantID}/v2.0`,
127
+ `https://sts.windows.net/${this.config.tenantID}/`,
128
+ ];
129
+
130
+ const payload = jwt.verify(idToken, publicKey, {
131
+ algorithms: ['RS256'],
132
+ audience: this.config.clientID!,
133
+ issuer: expectedIssuers,
134
+ }) as unknown as IdTokenPayload;
135
+
136
+ if (payload.nonce !== nonce) {
137
+ throw new EntraIDException('INVALID_NONCE');
138
+ }
139
+
140
+ return payload;
141
+ }
142
+
143
+ protected async buildUserJwt(
144
+ accessToken: string,
145
+ email: string,
146
+ name: string,
147
+ ): Promise<AuthResult> {
148
+ const msUser = await this.microsoftService.getMe(accessToken);
149
+
150
+ let groups: { displayName: string }[] = [];
151
+ try {
152
+ const raw = await this.microsoftService.getMemberOf(accessToken);
153
+ groups = raw.filter((g) => !!g.displayName);
154
+ } catch {}
155
+
156
+ const user: EntraUser = {
157
+ entraId: msUser.id,
158
+ email: msUser.mail ?? msUser.userPrincipalName ?? email,
159
+ name: msUser.displayName ?? name,
160
+ tenantId: this.config.tenantID,
161
+ matricula: msUser.employeeId,
162
+ groups,
163
+ };
164
+
165
+ const token = await this.internalTokenService.sign({ user, accessToken });
166
+
167
+ return { token, user };
168
+ }
169
+
170
+ protected createSignedState(): CreateStateNonce {
171
+ const { stateSecret } = this.config;
172
+ const nonce = CryptoUtils.generateHash();
173
+ const payload = Buffer.from(
174
+ JSON.stringify({ nonce, ts: Date.now() }),
175
+ ).toString('base64url');
176
+ const state = CryptoUtils.createSignedPayloadWithHMAC(payload, stateSecret);
177
+ return { nonce, state };
178
+ }
179
+
180
+ protected verifyStateAndReturnNonce(state: string): string {
181
+ const { stateSecret } = this.config;
182
+ const parts = state.split('.');
183
+
184
+ CryptoUtils.verifyPayloadWithHMAC(parts, stateSecret);
185
+
186
+ const payload = parts[0];
187
+
188
+ let nonce: string;
189
+ let ts: number;
190
+ try {
191
+ const parsed = JSON.parse(
192
+ Buffer.from(payload, 'base64url').toString(),
193
+ ) as { nonce: string; ts: number };
194
+ nonce = parsed.nonce;
195
+ ts = parsed.ts;
196
+ } catch {
197
+ throw new EntraIDException('STATE_MISMATCH');
198
+ }
199
+
200
+ if (Date.now() - ts > STATE_TTL_MS) {
201
+ throw new EntraIDException('STATE_MISMATCH');
202
+ }
203
+
204
+ return nonce;
205
+ }
206
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ CanActivate,
3
+ ExecutionContext,
4
+ Injectable,
5
+ UnauthorizedException,
6
+ } from '@nestjs/common';
7
+ import type { Request } from 'express';
8
+ import { InternalTokenService } from '../internal-token/internal-token.service';
9
+ import { EntraIDException } from '../customError/entraID.exception';
10
+ import { EntraUser } from '../types/entraID.type';
11
+ import { Reflector } from '@nestjs/core';
12
+ import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
13
+
14
+ @Injectable()
15
+ export class EntraAuthGuard implements CanActivate {
16
+ constructor(
17
+ private readonly internalTokenService: InternalTokenService,
18
+ private readonly reflector: Reflector,
19
+ ) {}
20
+
21
+ async canActivate(context: ExecutionContext): Promise<boolean> {
22
+ const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
23
+ context.getHandler(),
24
+ context.getClass(),
25
+ ]);
26
+
27
+ if (isPublic) return true;
28
+
29
+ const request = context.switchToHttp().getRequest();
30
+ const token = this.extractToken(request);
31
+
32
+ if (!token) throw new UnauthorizedException();
33
+
34
+ let payload: EntraUser;
35
+ try {
36
+ payload = this.internalTokenService.verify(token);
37
+ } catch (error) {
38
+ if (error instanceof EntraIDException) {
39
+ throw new UnauthorizedException(error.message);
40
+ }
41
+ throw error;
42
+ }
43
+
44
+ const revoke = await this.isTokenRevoked(payload, request);
45
+
46
+ if (revoke) {
47
+ throw new UnauthorizedException('TOKEN_REVOKED');
48
+ }
49
+
50
+ request.user = await this.transformPayload(payload);
51
+
52
+ return true;
53
+ }
54
+
55
+ protected async isTokenRevoked(
56
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
57
+ _payload: EntraUser,
58
+
59
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
60
+ _request: Request,
61
+ ): Promise<boolean> {
62
+ return false;
63
+ }
64
+
65
+ protected async transformPayload(payload: EntraUser): Promise<unknown> {
66
+ return payload;
67
+ }
68
+
69
+ protected extractToken(request: {
70
+ headers: { authorization?: string };
71
+ cookies?: { auth_token?: string };
72
+ }): string | undefined {
73
+ const auth = request.headers.authorization;
74
+ if (auth?.startsWith('Bearer ')) return auth.slice(7);
75
+ return request.cookies?.auth_token;
76
+ }
77
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ CanActivate,
3
+ ExecutionContext,
4
+ ForbiddenException,
5
+ Injectable,
6
+ } from '@nestjs/common';
7
+ import { Reflector } from '@nestjs/core';
8
+ import { ROLES_KEY } from '../decorators/roles.decorator';
9
+
10
+ @Injectable()
11
+ export class RolesGuard implements CanActivate {
12
+ constructor(private readonly reflector: Reflector) {}
13
+
14
+ canActivate(context: ExecutionContext): boolean {
15
+ const required = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
16
+ context.getHandler(),
17
+ context.getClass(),
18
+ ]);
19
+
20
+ if (!required?.length) return true;
21
+
22
+ const { user } = context.switchToHttp().getRequest();
23
+ const userGroups: string[] = (user?.groups ?? []).map(
24
+ (g: { displayName: string }) => g.displayName,
25
+ );
26
+
27
+ const allowed = required.some((role) => userGroups.includes(role));
28
+ if (!allowed) {
29
+ throw new ForbiddenException('INVALID_GROUP');
30
+ }
31
+
32
+ return true;
33
+ }
34
+ }
@@ -0,0 +1,12 @@
1
+ import {
2
+ AuthResult,
3
+ CallbackParams,
4
+ UrlRedirectSso,
5
+ } from '../types/entraID.type';
6
+
7
+ export interface IEntraIDService {
8
+ handleCallback(params: CallbackParams): Promise<AuthResult>;
9
+ initiateLogin(): Promise<UrlRedirectSso>;
10
+ refresh(refreshToken: string): Promise<AuthResult>;
11
+ logout(userId: string): Promise<void>;
12
+ }
@@ -0,0 +1,8 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { InternalTokenService } from './internal-token.service';
3
+
4
+ @Module({
5
+ providers: [InternalTokenService],
6
+ exports: [InternalTokenService],
7
+ })
8
+ export class InternalTokenModule {}