Skip to content

Commit d56da34

Browse files
authored
feat: add empty auth config service (#311)
openmfp/portal#626
1 parent c25ad52 commit d56da34

13 files changed

+255
-40
lines changed

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,21 @@ It is closely related to the [portal ui library](https://github.com/openmfp/port
1111
The main features of this library are:
1212

1313
- Provide a Dynamic Luigi configuration without the need to deploy a library
14-
- Authentication capabilities with GitHub and Auth Server
14+
- Optional authentication capabilities with GitHub and Auth Server
1515
- Dynamic development capabilities - Embed your local MicroFrontend into a running luigi frame.
16+
- Can run without any authentication infrastructure
1617

1718
# Getting started
1819

1920
## Set up an environment
2021

21-
To be able to use the library, the following environment properties have to be provided:
22+
The portal can run without any authentication infrastructure. Authentication configuration is optional and only required if you want to enable user authentication.
2223

23-
- **Mandatory**
24+
### Environment properties
25+
26+
- **Optional - Authentication**
27+
28+
> **Note:** All authentication-related environment variables are optional. The portal can run without any auth services to display node configurations. Configure these only if you want to enable authentication features.
2429
2530
| Property name | Description |
2631
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -44,17 +49,19 @@ To be able to use the library, the following environment properties have to be p
4449
| VALID_WEBCOMPONENT_URLS | To enable CORS Web component Loading: basically you need to add external domains where the Web Components are hosted; `".?"` in this examle, we are sepcify that we can load Web Components from everyhere. |
4550
| FEATURE_TOGGLES | Comma separated values of features following the convention `featureName=boolean`. Boolean value indicates is the feature is on/off (true/false) |
4651

47-
Below is an example of a `.env` file for configuring the application:
52+
#### Full configuration (with authentication)
4853

4954
```properties
5055
## Mandatory
5156
CONTENT_CONFIGURATION_VALIDATOR_API_URL=https://example.com/validate
57+
58+
## Auth Optional
5259
IDP_NAMES=app,dev
5360
BASE_DOMAINS_APP=localhost,example.com
5461
AUTH_SERVER_URL_APP=https://example.com/auth
5562
TOKEN_URL_APP=https://example.com/token
5663
OIDC_CLIENT_ID_APP=app_client_id
57-
OIDC_CLIENT_SECRET_APP= app_client_secret
64+
OIDC_CLIENT_SECRET_APP=app_client_secret
5865

5966
## Portal
6067
OPENMFP_PORTAL_CONTEXT_CRD_GATEWAY_API_URL=https://example.com/graphql

src/auth/auth-config.service.spec.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,41 @@
11
import { DiscoveryService, EnvService } from '../env/index.js';
2-
import { EnvAuthConfigService } from './auth-config.service.js';
2+
import {
3+
EmptyAuthConfigService,
4+
EnvAuthConfigService,
5+
} from './auth-config.service.js';
36
import { HttpException, HttpStatus } from '@nestjs/common';
47
import { Test, TestingModule } from '@nestjs/testing';
58
import type { Request } from 'express';
69
import { mock } from 'jest-mock-extended';
710

11+
describe('EmptyAuthConfigService', () => {
12+
let service: EmptyAuthConfigService;
13+
14+
beforeEach(() => {
15+
service = new EmptyAuthConfigService();
16+
});
17+
18+
describe('getAuthConfig', () => {
19+
it('should return empty object', async () => {
20+
const request = mock<Request>();
21+
request.hostname = 'example.com';
22+
23+
const result = await service.getAuthConfig(request);
24+
25+
expect(result).toEqual({});
26+
});
27+
28+
it('should return empty object for any hostname', async () => {
29+
const request = mock<Request>();
30+
request.hostname = 'test.localhost';
31+
32+
const result = await service.getAuthConfig(request);
33+
34+
expect(result).toEqual({});
35+
});
36+
});
37+
});
38+
839
describe('EnvAuthConfigService', () => {
940
let service: EnvAuthConfigService;
1041
let discoveryServiceMock: DiscoveryService;

src/auth/auth-config.service.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,29 @@ interface BaseDomainsToIdp {
99
}
1010

1111
export interface ServerAuthVariables {
12-
idpName: string;
13-
baseDomain: string;
14-
oauthServerUrl: string;
15-
oauthTokenUrl: string;
16-
clientId: string;
17-
clientSecret: string;
18-
oidcIssuerUrl: string;
12+
idpName?: string;
13+
baseDomain?: string;
14+
oauthServerUrl?: string;
15+
oauthTokenUrl?: string;
16+
clientId?: string;
17+
clientSecret?: string;
18+
oidcIssuerUrl?: string;
1919
endSessionUrl?: string;
2020
}
2121

2222
export interface AuthConfigService {
2323
getAuthConfig(request: Request): Promise<ServerAuthVariables>;
2424
}
2525

26+
@Injectable()
27+
export class EmptyAuthConfigService implements AuthConfigService {
28+
constructor() {}
29+
30+
public async getAuthConfig(request: Request): Promise<ServerAuthVariables> {
31+
return {};
32+
}
33+
}
34+
2635
@Injectable()
2736
export class EnvAuthConfigService implements AuthConfigService {
2837
constructor(

src/auth/auth-token.service.spec.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { EnvService } from '../env/index.js';
2-
import { AUTH_CALLBACK_INJECTION_TOKEN } from '../injection-tokens.js';
2+
import {
3+
AUTH_CALLBACK_INJECTION_TOKEN,
4+
AUTH_CONFIG_INJECTION_TOKEN,
5+
} from '../injection-tokens.js';
36
import { PortalModule } from '../portal.module.js';
47
import {
58
AuthConfigService,
9+
EmptyAuthConfigService,
610
EnvAuthConfigService,
711
} from './auth-config.service.js';
812
import { AuthTokenData, AuthTokenService } from './auth-token.service.js';
@@ -30,15 +34,21 @@ describe('AuthTokenService', () => {
3034

3135
authCallbackMock = mock<AuthCallback>();
3236
const module: TestingModule = await Test.createTestingModule({
33-
imports: [PortalModule.create({})],
37+
imports: [
38+
PortalModule.create({
39+
authConfigProvider: EnvAuthConfigService,
40+
}),
41+
],
3442
})
3543
.overrideProvider(AUTH_CALLBACK_INJECTION_TOKEN)
3644
.useValue(authCallbackMock)
3745
.compile();
3846

3947
service = module.get<AuthTokenService>(AuthTokenService);
4048
envService = module.get<EnvService>(EnvService);
41-
authConfigService = module.get<AuthConfigService>(EnvAuthConfigService);
49+
authConfigService = module.get<AuthConfigService>(
50+
AUTH_CONFIG_INJECTION_TOKEN,
51+
);
4252
responseMock = mock<Response>();
4353
requestMock = mock<Request>();
4454
});
@@ -128,6 +138,29 @@ describe('AuthTokenService', () => {
128138
'Unexpected response code from auth token server: 206, Partial Content',
129139
);
130140
});
141+
142+
it('should throw error when clientId is not configured', async () => {
143+
const module: TestingModule = await Test.createTestingModule({
144+
imports: [
145+
PortalModule.create({
146+
authConfigProvider: EmptyAuthConfigService,
147+
}),
148+
],
149+
})
150+
.overrideProvider(AUTH_CALLBACK_INJECTION_TOKEN)
151+
.useValue(authCallbackMock)
152+
.compile();
153+
154+
const emptyService = module.get<AuthTokenService>(AuthTokenService);
155+
156+
await expect(
157+
emptyService.exchangeTokenForRefreshToken(
158+
requestMock,
159+
responseMock,
160+
refreshToken,
161+
),
162+
).rejects.toThrow('Client ID is not configured');
163+
});
131164
});
132165

133166
describe('token for code - authorization_code flow', () => {
@@ -182,6 +215,29 @@ describe('AuthTokenService', () => {
182215
// Assert
183216
assertResponseAndCookies(authTokenResponse);
184217
});
218+
219+
it('should throw error when clientId is not configured', async () => {
220+
const module: TestingModule = await Test.createTestingModule({
221+
imports: [
222+
PortalModule.create({
223+
authConfigProvider: EmptyAuthConfigService,
224+
}),
225+
],
226+
})
227+
.overrideProvider(AUTH_CALLBACK_INJECTION_TOKEN)
228+
.useValue(authCallbackMock)
229+
.compile();
230+
231+
const emptyService = module.get<AuthTokenService>(AuthTokenService);
232+
233+
await expect(
234+
emptyService.exchangeTokenForCode(
235+
requestMock,
236+
responseMock,
237+
'test-code',
238+
),
239+
).rejects.toThrow('Client ID is not configured');
240+
});
185241
});
186242

187243
it('handles an auth server error', async () => {

src/auth/auth-token.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ export class AuthTokenService {
4444
const authConfig = await this.authConfigService.getAuthConfig(request);
4545
const redirectUri = getRedirectUri(request);
4646

47+
if (!authConfig.clientId) {
48+
throw new Error('Client ID is not configured');
49+
}
50+
4751
const body = new URLSearchParams({
4852
client_id: authConfig.clientId,
4953
grant_type: 'authorization_code',
@@ -66,6 +70,10 @@ export class AuthTokenService {
6670
): Promise<AuthTokenData> {
6771
const authConfig = await this.authConfigService.getAuthConfig(request);
6872

73+
if (!authConfig.clientId) {
74+
throw new Error('Client ID is not configured');
75+
}
76+
6977
const body = new URLSearchParams({
7078
grant_type: 'refresh_token',
7179
refresh_token: refreshToken,

src/auth/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
export { AuthController } from './auth.controller.js';
1+
export {
2+
AuthConfigService,
3+
EnvAuthConfigService,
4+
ServerAuthVariables,
5+
} from './auth-config.service.js';
6+
export { AuthTokenData, AuthTokenService } from './auth-token.service.js';
27
export * from './auth.callback.js';
3-
export * from './auth-config.service.js';
4-
export { AuthTokenService, AuthTokenData } from './auth-token.service.js';
58
export { AuthCallback } from './auth.callback.js';
9+
export { AuthController } from './auth.controller.js';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { RawServiceProvider } from './service-provider.js';
2+
3+
export const DEFAULT_SERVICE_PROVIDERS: RawServiceProvider[] = [
4+
{
5+
name: 'getting-started',
6+
displayName: 'Getting Started',
7+
creationTimestamp: '',
8+
contentConfiguration: [
9+
{
10+
name: 'getting-started',
11+
creationTimestamp: '',
12+
luigiConfigFragment: {
13+
data: {
14+
nodes: [
15+
{
16+
entityType: 'global',
17+
pathSegment: 'home',
18+
label: 'Overview',
19+
icon: 'home',
20+
hideFromNav: true,
21+
defineEntity: {
22+
id: 'example',
23+
},
24+
viewUrl: '/home',
25+
children: [
26+
{
27+
pathSegment: 'overview',
28+
viewUrl: '/overview',
29+
label: 'Overview',
30+
icon: 'home',
31+
url: '/assets/openmfp-portal-ui-wc.js#getting-started',
32+
webcomponent: {
33+
selfRegistered: true,
34+
},
35+
},
36+
],
37+
},
38+
],
39+
},
40+
},
41+
},
42+
],
43+
},
44+
];
Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1+
import { DEFAULT_SERVICE_PROVIDERS } from './service-provider-default.js';
12
import {
2-
EmptyServiceProviderService,
3+
DefaultServiceProviderService,
34
ServiceProviderService,
4-
} from './service-provider';
5+
} from './service-provider.js';
56
import { Test, TestingModule } from '@nestjs/testing';
67

7-
describe('EmptyServiceProviderService', () => {
8+
describe('DefaultServiceProviderService', () => {
89
let service: ServiceProviderService;
910

1011
beforeEach(async () => {
1112
const module: TestingModule = await Test.createTestingModule({
12-
providers: [EmptyServiceProviderService],
13+
providers: [DefaultServiceProviderService],
1314
}).compile();
1415

15-
service = module.get<ServiceProviderService>(EmptyServiceProviderService);
16+
service = module.get<ServiceProviderService>(DefaultServiceProviderService);
1617
});
1718

18-
it('should return an empty serviceProviders', async () => {
19+
it('should return an default serviceProviders', async () => {
1920
const response = await service.getServiceProviders('token', [], {});
2021

21-
expect(response).toEqual({ rawServiceProviders: [] });
22+
expect(response).toEqual({
23+
rawServiceProviders: DEFAULT_SERVICE_PROVIDERS,
24+
});
2225
});
2326
});

src/config/context/service-provider.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ContentConfiguration } from '../model/content-configuration.js';
22
import { StackSearch } from '../model/luigi.node.js';
3+
import { DEFAULT_SERVICE_PROVIDERS } from './service-provider-default.js';
34

45
export interface HelpCenterData {
56
stackSearch?: StackSearch;
@@ -32,10 +33,10 @@ export interface ServiceProviderService {
3233
): Promise<ServiceProviderResponse>;
3334
}
3435

35-
export class EmptyServiceProviderService implements ServiceProviderService {
36+
export class DefaultServiceProviderService implements ServiceProviderService {
3637
getServiceProviders(): Promise<ServiceProviderResponse> {
3738
return Promise.resolve({
38-
rawServiceProviders: [],
39+
rawServiceProviders: DEFAULT_SERVICE_PROVIDERS,
3940
});
4041
}
41-
}
42+
}

src/config/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export {
22
ServiceProviderResponse,
33
ServiceProviderService,
44
RawServiceProvider,
5-
EmptyServiceProviderService,
5+
DefaultServiceProviderService,
66
HelpCenterData,
77
} from './context/service-provider.js';
88
export * from './model/content-configuration.js';

0 commit comments

Comments
 (0)