Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ pids

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
/yalc.lock
/.yalc/
10 changes: 6 additions & 4 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ module.exports = {
reporters: ['default'],
coverageThreshold: {
global: {
branches: 78,
functions: 80,
branches: 80,
functions: 95,
lines: 95,
statements: -10,
statements: -3,
},
},
coveragePathIgnorePatterns: ['/node_modules/', '/integration-tests/'],
coverageDirectory: '../test-run-reports/coverage/unit',
transformIgnorePatterns: ['/node_modules/(?!(@openmfp/portal-server-lib|graphql-request)/)'],
transformIgnorePatterns: [
'/node_modules/(?!(@openmfp/portal-server-lib|graphql-request)/)',
],
transform: {
'^.+\\.(t|j)s$': [
'ts-jest',
Expand Down
22 changes: 8 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@
"import": "./dist/portal-options/index.js",
"types": "./dist/portal-options/index.d.ts"
},
"./services": {
"import": "./dist/services/index.js",
"types": "./dist/services/index.d.ts"
},
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "nest build",
"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\"",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
Expand All @@ -43,6 +48,7 @@
"@nestjs/axios": ">=3.0.0",
"@nestjs/common": ">=10.0.0",
"@nestjs/serve-static": ">=4.0.0",
"@openmfp/portal-server-lib": ">=0.158.1",
"axios": ">=1.7.7",
"express": ">=4.21.1",
"rxjs": ">=7.8.1"
Expand Down Expand Up @@ -73,5 +79,6 @@
"tsconfig-paths": "4.2.0",
"typescript": "5.8.3",
"typescript-eslint": "^8.0.0"
}
},
"type": "module"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AuthCallbackProvider } from './auth-callback-provider';
import { IAMGraphQlService } from './iam-graphql.service';
import { AuthCallbackProvider } from './auth-callback-provider.js';
import { IAMGraphQlService } from './services/iam-graphql.service.js';
import { Test, TestingModule } from '@nestjs/testing';
import type { AuthTokenData } from '@openmfp/portal-server-lib';
import type { Request, Response } from 'express';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IAMGraphQlService } from './iam-graphql.service.js';
import { IAMGraphQlService } from './services/iam-graphql.service.js';
import { Injectable, Logger } from '@nestjs/common';
import { AuthCallback, AuthTokenData } from '@openmfp/portal-server-lib';
import { Request, Response } from 'express';
Expand Down
91 changes: 91 additions & 0 deletions src/portal-options/auth-config-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { PMAuthConfigProvider } from './auth-config-provider.js';
import { HttpException } from '@nestjs/common';
import {
DiscoveryService,
EnvAuthConfigService,
} from '@openmfp/portal-server-lib';
import type { Request } from 'express';
import { mock } from 'jest-mock-extended';

describe('PMAuthConfigProvider', () => {
let provider: PMAuthConfigProvider;
let discoveryService: jest.Mocked<DiscoveryService>;
let envAuthConfigService: jest.Mocked<EnvAuthConfigService>;

beforeEach(() => {
discoveryService = mock<DiscoveryService>();
envAuthConfigService = mock<EnvAuthConfigService>();
provider = new PMAuthConfigProvider(discoveryService, envAuthConfigService);
jest.resetModules();
process.env = {
AUTH_SERVER_URL_DEFAULT: 'authUrl',
TOKEN_URL_DEFAULT: 'tokenUrl',
BASE_DOMAINS_DEFAULT: 'example.com',
OIDC_CLIENT_ID_DEFAULT: 'client123',
OIDC_CLIENT_SECRET_DEFAULT: 'secret123',
};
});

it('should delegate to EnvAuthConfigService if available', async () => {
const req = { hostname: 'foo.example.com' } as Request;
const expected = {
idpName: 'idp',
baseDomain: 'example.com',
oauthServerUrl: 'url',
oauthTokenUrl: 'token',
clientId: 'cid',
clientSecret: 'sec',
};
envAuthConfigService.getAuthConfig.mockResolvedValue(expected);

const result = await provider.getAuthConfig(req);

expect(result).toEqual(expected);
});

it('should fall back to default configuration if EnvAuthConfigService throws', async () => {
const req = { hostname: 'foo.example.com' } as Request;
envAuthConfigService.getAuthConfig.mockRejectedValue(new Error('fail'));
discoveryService.getOIDC.mockResolvedValue({
authorization_endpoint: 'authUrl',
token_endpoint: 'tokenUrl',
});

const result = await provider.getAuthConfig(req);

expect(result).toMatchObject({
baseDomain: 'example.com',
oauthServerUrl: 'authUrl',
oauthTokenUrl: 'tokenUrl',
clientId: 'client123',
clientSecret: 'secret123',
});
});

it('should throw if default configuration incomplete', async () => {
const req = { hostname: 'foo.example.com' } as Request;
envAuthConfigService.getAuthConfig.mockRejectedValue(new Error('fail'));
discoveryService.getOIDC.mockResolvedValue(null);
process.env = {};

await expect(provider.getAuthConfig(req)).rejects.toThrow(HttpException);
});

it('getDomain should return organization and baseDomain', () => {
const req = { hostname: 'foo.example.com' } as Request;
const result = provider.getDomain(req);
expect(result).toEqual({
organization: 'foo',
baseDomain: 'example.com',
});
});

it('getDomain should return clientId if hostname equals baseDomain', () => {
const req = { hostname: 'example.com' } as Request;
const result = provider.getDomain(req);
expect(result).toEqual({
organization: 'client123',
baseDomain: 'example.com',
});
});
});
75 changes: 75 additions & 0 deletions src/portal-options/auth-config-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import {
AuthConfigService,
DiscoveryService,
EnvAuthConfigService,
ServerAuthVariables,
} from '@openmfp/portal-server-lib';
import type { Request } from 'express';

@Injectable()
export class PMAuthConfigProvider implements AuthConfigService {
private logger: Logger = new Logger(PMAuthConfigProvider.name);

constructor(
private discoveryService: DiscoveryService,
private envEuthConfigService: EnvAuthConfigService,
) {}

async getAuthConfig(request: Request): Promise<ServerAuthVariables> {
try {
return await this.envEuthConfigService.getAuthConfig(request);
} catch {
this.logger.debug(
'Failed to retrieve auth config from environment variables based on provided IDP.',
);
}

this.logger.debug('Resolving auth config from default configuration.');

const oidc = await this.discoveryService.getOIDC('DEFAULT');
const oauthServerUrl =
oidc?.authorization_endpoint ?? process.env['AUTH_SERVER_URL_DEFAULT'];
const oauthTokenUrl =
oidc?.token_endpoint ?? process.env['TOKEN_URL_DEFAULT'];

const baseDomain = process.env['BASE_DOMAINS_DEFAULT'];
const clientId = process.env['OIDC_CLIENT_ID_DEFAULT'];
const clientSecretEnvVar = 'OIDC_CLIENT_SECRET_DEFAULT';
const clientSecret = process.env[clientSecretEnvVar];

if (!oauthServerUrl || !oauthTokenUrl || !clientId || !clientSecret) {
const hasClientSecret = !!clientSecret;
throw new HttpException(
{
message: 'Default auth configuration incomplete.',
error: `The default properly configured. oauthServerUrl: '${oauthServerUrl}' oauthTokenUrl: '${oauthTokenUrl}' clientId: '${clientId}', has client secret (${clientSecretEnvVar}): ${String(
hasClientSecret,
)}`,
statusCode: HttpStatus.NOT_FOUND,
},
HttpStatus.NOT_FOUND,
);
}

const subDomain = request.hostname.split('.')[0];
return {
idpName: request.hostname === baseDomain ? clientId : subDomain,
baseDomain,
oauthServerUrl,
clientId,
clientSecret,
oauthTokenUrl,
};
}

getDomain(request: Request): { organization?: string; baseDomain?: string } {
const subDomain = request.hostname.split('.')[0];
const clientId = process.env['OIDC_CLIENT_ID_DEFAULT'];
const baseDomain = process.env['BASE_DOMAINS_DEFAULT'];
return {
organization: request.hostname === baseDomain ? clientId : subDomain,
baseDomain,
};
}
}
11 changes: 6 additions & 5 deletions src/portal-options/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './entity-context-provider/account-entity-context-provider.service.js';
export * from './portal-context-provider/openmfp-portal-context.service.js';
export * from './request-context-provider/openmfp-request-context-provider.js';
export * from './account-entity-context-provider.service.js';
export * from './openmfp-portal-context.service.js';
export * from './openmfp-request-context-provider.js';
export * from './auth-config-provider.js';
export * from './service-providers/content-configuration-service-providers.service.js';
export * from './service-providers/iam/auth-callback-provider.js';
export * from './service-providers/iam/iam-graphql.service.js';
export * from './auth-callback-provider.js';
export * from './services/iam-graphql.service.js';
Loading