Skip to content

Commit 34b4277

Browse files
authored
Merge pull request #8 from platform-mesh/feat/add-a-custom-auth-config-provider
feat: add a custom auth config provider
2 parents 9fa852d + 987ac19 commit 34b4277

23 files changed

+331
-111
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,5 @@ pids
5555

5656
# Diagnostic reports (https://nodejs.org/api/report.html)
5757
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
58+
/yalc.lock
59+
/.yalc/

jest.config.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ module.exports = {
99
reporters: ['default'],
1010
coverageThreshold: {
1111
global: {
12-
branches: 78,
13-
functions: 80,
12+
branches: 80,
13+
functions: 95,
1414
lines: 95,
15-
statements: -10,
15+
statements: -3,
1616
},
1717
},
1818
coveragePathIgnorePatterns: ['/node_modules/', '/integration-tests/'],
1919
coverageDirectory: '../test-run-reports/coverage/unit',
20-
transformIgnorePatterns: ['/node_modules/(?!(@openmfp/portal-server-lib|graphql-request)/)'],
20+
transformIgnorePatterns: [
21+
'/node_modules/(?!(@openmfp/portal-server-lib|graphql-request)/)',
22+
],
2123
transform: {
2224
'^.+\\.(t|j)s$': [
2325
'ts-jest',

package-lock.json

Lines changed: 8 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@
1111
"import": "./dist/portal-options/index.js",
1212
"types": "./dist/portal-options/index.d.ts"
1313
},
14+
"./services": {
15+
"import": "./dist/services/index.js",
16+
"types": "./dist/services/index.d.ts"
17+
},
1418
".": {
1519
"import": "./dist/index.js",
1620
"types": "./dist/index.d.ts"
1721
}
1822
},
1923
"scripts": {
2024
"build": "nest build",
25+
"build:watch": "mkdirp dist && nodemon --ignore dist --ext js,yml,yaml,ts,html,css,scss,json,md --exec \"rimraf dist && npm run build && yalc publish --push --sig\"",
2126
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"",
2227
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
2328
"test": "jest",
@@ -43,6 +48,7 @@
4348
"@nestjs/axios": ">=3.0.0",
4449
"@nestjs/common": ">=10.0.0",
4550
"@nestjs/serve-static": ">=4.0.0",
51+
"@openmfp/portal-server-lib": ">=0.158.1",
4652
"axios": ">=1.7.7",
4753
"express": ">=4.21.1",
4854
"rxjs": ">=7.8.1"
@@ -73,5 +79,6 @@
7379
"tsconfig-paths": "4.2.0",
7480
"typescript": "5.8.3",
7581
"typescript-eslint": "^8.0.0"
76-
}
82+
},
83+
"type": "module"
7784
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { AuthCallbackProvider } from './auth-callback-provider';
2-
import { IAMGraphQlService } from './iam-graphql.service';
1+
import { AuthCallbackProvider } from './auth-callback-provider.js';
2+
import { IAMGraphQlService } from './services/iam-graphql.service.js';
33
import { Test, TestingModule } from '@nestjs/testing';
44
import type { AuthTokenData } from '@openmfp/portal-server-lib';
55
import type { Request, Response } from 'express';
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IAMGraphQlService } from './iam-graphql.service.js';
1+
import { IAMGraphQlService } from './services/iam-graphql.service.js';
22
import { Injectable, Logger } from '@nestjs/common';
33
import { AuthCallback, AuthTokenData } from '@openmfp/portal-server-lib';
44
import { Request, Response } from 'express';
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { PMAuthConfigProvider } from './auth-config-provider.js';
2+
import { HttpException } from '@nestjs/common';
3+
import {
4+
DiscoveryService,
5+
EnvAuthConfigService,
6+
} from '@openmfp/portal-server-lib';
7+
import type { Request } from 'express';
8+
import { mock } from 'jest-mock-extended';
9+
10+
describe('PMAuthConfigProvider', () => {
11+
let provider: PMAuthConfigProvider;
12+
let discoveryService: jest.Mocked<DiscoveryService>;
13+
let envAuthConfigService: jest.Mocked<EnvAuthConfigService>;
14+
15+
beforeEach(() => {
16+
discoveryService = mock<DiscoveryService>();
17+
envAuthConfigService = mock<EnvAuthConfigService>();
18+
provider = new PMAuthConfigProvider(discoveryService, envAuthConfigService);
19+
jest.resetModules();
20+
process.env = {
21+
AUTH_SERVER_URL_DEFAULT: 'authUrl',
22+
TOKEN_URL_DEFAULT: 'tokenUrl',
23+
BASE_DOMAINS_DEFAULT: 'example.com',
24+
OIDC_CLIENT_ID_DEFAULT: 'client123',
25+
OIDC_CLIENT_SECRET_DEFAULT: 'secret123',
26+
};
27+
});
28+
29+
it('should delegate to EnvAuthConfigService if available', async () => {
30+
const req = { hostname: 'foo.example.com' } as Request;
31+
const expected = {
32+
idpName: 'idp',
33+
baseDomain: 'example.com',
34+
oauthServerUrl: 'url',
35+
oauthTokenUrl: 'token',
36+
clientId: 'cid',
37+
clientSecret: 'sec',
38+
};
39+
envAuthConfigService.getAuthConfig.mockResolvedValue(expected);
40+
41+
const result = await provider.getAuthConfig(req);
42+
43+
expect(result).toEqual(expected);
44+
});
45+
46+
it('should fall back to default configuration if EnvAuthConfigService throws', async () => {
47+
const req = { hostname: 'foo.example.com' } as Request;
48+
envAuthConfigService.getAuthConfig.mockRejectedValue(new Error('fail'));
49+
discoveryService.getOIDC.mockResolvedValue({
50+
authorization_endpoint: 'authUrl',
51+
token_endpoint: 'tokenUrl',
52+
});
53+
54+
const result = await provider.getAuthConfig(req);
55+
56+
expect(result).toMatchObject({
57+
baseDomain: 'example.com',
58+
oauthServerUrl: 'authUrl',
59+
oauthTokenUrl: 'tokenUrl',
60+
clientId: 'client123',
61+
clientSecret: 'secret123',
62+
});
63+
});
64+
65+
it('should throw if default configuration incomplete', async () => {
66+
const req = { hostname: 'foo.example.com' } as Request;
67+
envAuthConfigService.getAuthConfig.mockRejectedValue(new Error('fail'));
68+
discoveryService.getOIDC.mockResolvedValue(null);
69+
process.env = {};
70+
71+
await expect(provider.getAuthConfig(req)).rejects.toThrow(HttpException);
72+
});
73+
74+
it('getDomain should return organization and baseDomain', () => {
75+
const req = { hostname: 'foo.example.com' } as Request;
76+
const result = provider.getDomain(req);
77+
expect(result).toEqual({
78+
organization: 'foo',
79+
baseDomain: 'example.com',
80+
});
81+
});
82+
83+
it('getDomain should return clientId if hostname equals baseDomain', () => {
84+
const req = { hostname: 'example.com' } as Request;
85+
const result = provider.getDomain(req);
86+
expect(result).toEqual({
87+
organization: 'client123',
88+
baseDomain: 'example.com',
89+
});
90+
});
91+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
2+
import {
3+
AuthConfigService,
4+
DiscoveryService,
5+
EnvAuthConfigService,
6+
ServerAuthVariables,
7+
} from '@openmfp/portal-server-lib';
8+
import type { Request } from 'express';
9+
10+
@Injectable()
11+
export class PMAuthConfigProvider implements AuthConfigService {
12+
private logger: Logger = new Logger(PMAuthConfigProvider.name);
13+
14+
constructor(
15+
private discoveryService: DiscoveryService,
16+
private envEuthConfigService: EnvAuthConfigService,
17+
) {}
18+
19+
async getAuthConfig(request: Request): Promise<ServerAuthVariables> {
20+
try {
21+
return await this.envEuthConfigService.getAuthConfig(request);
22+
} catch {
23+
this.logger.debug(
24+
'Failed to retrieve auth config from environment variables based on provided IDP.',
25+
);
26+
}
27+
28+
this.logger.debug('Resolving auth config from default configuration.');
29+
30+
const oidc = await this.discoveryService.getOIDC('DEFAULT');
31+
const oauthServerUrl =
32+
oidc?.authorization_endpoint ?? process.env['AUTH_SERVER_URL_DEFAULT'];
33+
const oauthTokenUrl =
34+
oidc?.token_endpoint ?? process.env['TOKEN_URL_DEFAULT'];
35+
36+
const baseDomain = process.env['BASE_DOMAINS_DEFAULT'];
37+
const clientId = process.env['OIDC_CLIENT_ID_DEFAULT'];
38+
const clientSecretEnvVar = 'OIDC_CLIENT_SECRET_DEFAULT';
39+
const clientSecret = process.env[clientSecretEnvVar];
40+
41+
if (!oauthServerUrl || !oauthTokenUrl || !clientId || !clientSecret) {
42+
const hasClientSecret = !!clientSecret;
43+
throw new HttpException(
44+
{
45+
message: 'Default auth configuration incomplete.',
46+
error: `The default properly configured. oauthServerUrl: '${oauthServerUrl}' oauthTokenUrl: '${oauthTokenUrl}' clientId: '${clientId}', has client secret (${clientSecretEnvVar}): ${String(
47+
hasClientSecret,
48+
)}`,
49+
statusCode: HttpStatus.NOT_FOUND,
50+
},
51+
HttpStatus.NOT_FOUND,
52+
);
53+
}
54+
55+
const subDomain = request.hostname.split('.')[0];
56+
return {
57+
idpName: request.hostname === baseDomain ? clientId : subDomain,
58+
baseDomain,
59+
oauthServerUrl,
60+
clientId,
61+
clientSecret,
62+
oauthTokenUrl,
63+
};
64+
}
65+
66+
getDomain(request: Request): { organization?: string; baseDomain?: string } {
67+
const subDomain = request.hostname.split('.')[0];
68+
const clientId = process.env['OIDC_CLIENT_ID_DEFAULT'];
69+
const baseDomain = process.env['BASE_DOMAINS_DEFAULT'];
70+
return {
71+
organization: request.hostname === baseDomain ? clientId : subDomain,
72+
baseDomain,
73+
};
74+
}
75+
}

src/portal-options/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
export * from './entity-context-provider/account-entity-context-provider.service.js';
2-
export * from './portal-context-provider/openmfp-portal-context.service.js';
3-
export * from './request-context-provider/openmfp-request-context-provider.js';
1+
export * from './account-entity-context-provider.service.js';
2+
export * from './openmfp-portal-context.service.js';
3+
export * from './openmfp-request-context-provider.js';
4+
export * from './auth-config-provider.js';
45
export * from './service-providers/content-configuration-service-providers.service.js';
5-
export * from './service-providers/iam/auth-callback-provider.js';
6-
export * from './service-providers/iam/iam-graphql.service.js';
6+
export * from './auth-callback-provider.js';
7+
export * from './services/iam-graphql.service.js';

0 commit comments

Comments
 (0)