Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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',
});
});
});
73 changes: 73 additions & 0 deletions src/portal-options/auth-config-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
AuthConfigService,
DiscoveryService,
EnvAuthConfigService,
ServerAuthVariables,
} from '@openmfp/portal-server-lib';
import type { Request } from 'express';

@Injectable()
export class PMAuthConfigProvider implements AuthConfigService {
constructor(
private discoveryService: DiscoveryService,
private envEuthConfigService: EnvAuthConfigService,
) {}

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

console.log('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