Skip to content

Commit 0728489

Browse files
Use port from the request while calculating the redirection url. (#298)
Co-authored-by: Grzegorz Krajniak <[email protected]>
1 parent 255c9af commit 0728489

File tree

8 files changed

+114
-102
lines changed

8 files changed

+114
-102
lines changed

README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33
![Build Status](https://github.com/openmfp/portal-server-lib/actions/workflows/pipeline.yaml/badge.svg)
44
[![REUSE status](https://api.reuse.software/badge/github.com/openmfp/portal-server-lib)](https://api.reuse.software/info/github.com/openmfp/portal-server-lib)
55

6-
This library help you to set up a [Nest.js](https://nestjs.com/) server to serve a dynamic luigi configuration.
6+
This library helps you to set up a [Nest.js](https://nestjs.com/) server to serve a dynamic luigi configuration.
77
It is closely related to the [portal ui library](https://github.com/openmfp/portal-ui-lib).
88

9-
Main features of this library are:
9+
The main features of this library are:
1010

11-
- Provide a Dynamic Luigi configuration, without the need to deploy a library
11+
- Provide a Dynamic Luigi configuration without the need to deploy a library
1212
- Authentication capabilities with GitHub and Auth Server
1313
- Dynamic development capabilities - Embed your local MicroFrontend into a running luigi frame.
1414

1515
# Getting started
1616

17-
## Set up environment
17+
## Set up an environment
1818

19-
In order to be able to use the library following environment properties have to be provided:
19+
To be able to use the library, the following environment properties have to be provided:
2020

2121
- **Mandatory**
2222

@@ -32,15 +32,15 @@ In order to be able to use the library following environment properties have to
3232

3333
- **Optional**
3434

35-
| Property name | Description |
36-
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37-
| HEALTH_CHECK_INTERVAL | The interval in _milliseconds_ at which the application performs health checks to ensure its components are functioning correctly. Default 2000 ms. |
38-
| LOGOUT_REDIRECT_URL | The url to redirect user after logout action, by default _/logout_. |
39-
| ENVIRONMENT | This property indicates the environment in which the application is running, _local_ indicates development environment. |
40-
| DEVELOPMENT_INSTANCE | This property indicates if the portal runs in development mode. |
41-
| FRONTEND_PORT | Set the port number on which the frontend of the application will run in _local_ environment. |
35+
| Property name | Description |
36+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37+
| HEALTH_CHECK_INTERVAL | The interval in _milliseconds_ at which the application performs health checks to ensure its components are functioning correctly. Default 2000 ms. |
38+
| LOGOUT_REDIRECT_URL | The url to redirect user after logout action, by default _/logout_. |
39+
| ENVIRONMENT | This property indicates the environment in which the application is running, _local_ indicates development environment. |
40+
| DEVELOPMENT_INSTANCE | This property indicates if the portal runs in development mode. |
41+
| FRONTEND_PORT | Set the port number on which the frontend of the application will run. |
4242
| 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. |
43-
| FEATURE_TOGGLES | Comma separated values of features following the convention `featureName=boolean`. Boolean value indicates is the feature is on/off (true/false) |
43+
| FEATURE_TOGGLES | Comma separated values of features following the convention `featureName=boolean`. Boolean value indicates is the feature is on/off (true/false) |
4444

4545
Below is an example of a `.env` file for configuring the application:
4646

@@ -101,8 +101,8 @@ bootstrap();
101101

102102
## Requirements
103103

104-
The portal requires a installation of node.js and npm.
105-
Checkout the [package.json](package.json) for the required node version and dependencies.
104+
The portal requires an installation of node.js and npm.
105+
Check out the [package.json](package.json) for the required node version and dependencies.
106106

107107
## Contributing
108108

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ describe('AuthTokenService', () => {
188188
// Arrange
189189
requestMock.hostname = 'localhost';
190190
requestMock.protocol = 'http';
191-
process.env['FRONTEND_PORT'] = '4700';
191+
requestMock.headers = { host: 'localhost:4700' };
192192
const env = await authConfigService.getAuthConfig(requestMock);
193193
const code = 'secret code';
194194

@@ -211,7 +211,6 @@ describe('AuthTokenService', () => {
211211
await expect(authTokenResponsePromise).rejects.toThrow(
212212
'Error response from auth token server: AxiosError: Request failed with status code 500',
213213
);
214-
delete process.env['FRONTEND_PORT'];
215214
});
216215
});
217216
});

src/auth/auth-token.service.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import { EnvService } from '../env/index.js';
21
import { AUTH_CONFIG_INJECTION_TOKEN } from '../injection-tokens.js';
32
import { CookiesService } from '../services/index.js';
43
import {
54
AuthConfigService,
65
ServerAuthVariables,
76
} from './auth-config.service.js';
7+
import { getRedirectUri } from './redirect-uri.js';
88
import { HttpService } from '@nestjs/axios';
99
import { Inject, Injectable } from '@nestjs/common';
1010
import { AxiosError } from 'axios';
1111
import type { Request, Response } from 'express';
1212
import { catchError, firstValueFrom } from 'rxjs';
13-
import { getRedirectUri } from './redirect-uri.js';
1413

1514
export interface AuthTokenData {
1615
access_token: string;
@@ -27,7 +26,6 @@ export class AuthTokenService {
2726
constructor(
2827
@Inject(AUTH_CONFIG_INJECTION_TOKEN)
2928
private authConfigService: AuthConfigService,
30-
private envService: EnvService,
3129
private httpService: HttpService,
3230
private cookiesService: CookiesService,
3331
) {}
@@ -44,7 +42,7 @@ export class AuthTokenService {
4442
code: string,
4543
): Promise<AuthTokenData> {
4644
const authConfig = await this.authConfigService.getAuthConfig(request);
47-
const redirectUri = getRedirectUri(request, this.envService.getEnv());
45+
const redirectUri = getRedirectUri(request);
4846

4947
const body = new URLSearchParams({
5048
client_id: authConfig.clientId,

src/auth/redirect-uri.spec.ts

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,86 @@
11
import { getRedirectUri } from './redirect-uri.js';
22
import type { Request } from 'express';
3-
import { EnvVariables } from '../env/env.service.js';
43

54
describe('getRedirectUri', () => {
6-
const baseEnv: EnvVariables = {
7-
frontendPort: '3000',
8-
} as any;
5+
const makeReq = (options: Partial<Request>): Request =>
6+
({
7+
headers: options.headers || {},
8+
hostname: options.hostname || 'example.com',
9+
protocol: options.protocol || 'http',
10+
}) as Request;
911

10-
const makeReq = (headers: Record<string, any> = {}, hostname = 'example.com', protocol = 'http'): Request =>
11-
({
12-
headers,
13-
hostname,
14-
protocol,
15-
} as Request);
16-
17-
it('uses x-forwarded-proto and x-forwarded-host when present', () => {
18-
const req = makeReq({
19-
'x-forwarded-proto': 'https',
20-
'x-forwarded-host': 'forwarded.com',
21-
});
22-
const result = getRedirectUri(req, baseEnv);
23-
expect(result).toBe('https://forwarded.com:3000/callback?storageType=none');
12+
it('uses x-forwarded headers for proto, host, and port', () => {
13+
const req = makeReq({
14+
headers: {
15+
'x-forwarded-proto': 'https',
16+
'x-forwarded-host': 'forwarded.com',
17+
'x-forwarded-port': '8443',
18+
},
2419
});
20+
const result = getRedirectUri(req);
21+
expect(result).toBe('https://forwarded.com:8443/callback?storageType=none');
22+
});
2523

26-
it('uses first value when x-forwarded-proto and host are arrays', () => {
27-
const req = makeReq({
28-
'x-forwarded-proto': ['https', 'http'],
29-
'x-forwarded-host': ['multi.com:8080', 'other.com'],
30-
});
31-
const result = getRedirectUri(req, baseEnv);
32-
expect(result).toBe('https://multi.com:3000/callback?storageType=none');
24+
it('handles x-forwarded headers as arrays', () => {
25+
const req = makeReq({
26+
headers: {
27+
'x-forwarded-proto': ['https', 'http'],
28+
'x-forwarded-host': ['multi.com:8080', 'other.com'],
29+
'x-forwarded-port': ['8081', '8082'],
30+
},
3331
});
32+
const result = getRedirectUri(req);
33+
expect(result).toBe('https://multi.com:8081/callback?storageType=none');
34+
});
3435

35-
it('falls back to request protocol and hostname if headers missing', () => {
36-
const req = makeReq({}, 'local.dev', 'http');
37-
const result = getRedirectUri(req, baseEnv);
38-
expect(result).toBe('http://local.dev:3000/callback?storageType=none');
36+
it('omits standard ports 80 and 443', () => {
37+
const req80 = makeReq({
38+
headers: {
39+
'x-forwarded-proto': 'http',
40+
'x-forwarded-host': 'plain.com',
41+
'x-forwarded-port': '80',
42+
},
3943
});
40-
41-
it('omits port if standard (80 or 443)', () => {
42-
const env80 = { frontendPort: '80' } as EnvVariables;
43-
const env443 = { frontendPort: '443' } as EnvVariables;
44-
const req = makeReq({ 'x-forwarded-proto': 'https', 'x-forwarded-host': 'secure.com' });
45-
expect(getRedirectUri(req, env80)).toBe('https://secure.com/callback?storageType=none');
46-
expect(getRedirectUri(req, env443)).toBe('https://secure.com/callback?storageType=none');
44+
const req443 = makeReq({
45+
headers: {
46+
'x-forwarded-proto': 'https',
47+
'x-forwarded-host': 'secure.com',
48+
'x-forwarded-port': '443',
49+
},
4750
});
51+
expect(getRedirectUri(req80)).toBe(
52+
'http://plain.com/callback?storageType=none',
53+
);
54+
expect(getRedirectUri(req443)).toBe(
55+
'https://secure.com/callback?storageType=none',
56+
);
57+
});
4858

49-
it('omits port if env.frontendPort is empty', () => {
50-
const envEmpty = { frontendPort: '' } as EnvVariables;
51-
const req = makeReq({ 'x-forwarded-proto': 'https', 'x-forwarded-host': 'noport.com' });
52-
const result = getRedirectUri(req, envEmpty);
53-
expect(result).toBe('https://noport.com/callback?storageType=none');
59+
it('falls back to host header port when x-forwarded-port missing', () => {
60+
const req = makeReq({
61+
headers: {
62+
host: 'local.dev:3000',
63+
'x-forwarded-proto': 'http',
64+
},
5465
});
66+
const result = getRedirectUri(req);
67+
expect(result).toBe('http://example.com:3000/callback?storageType=none');
68+
});
69+
70+
it('falls back to request protocol and hostname if no headers', () => {
71+
const req = makeReq({ hostname: 'app.local', protocol: 'http' });
72+
const result = getRedirectUri(req);
73+
expect(result).toBe('http://app.local/callback?storageType=none');
74+
});
5575

56-
it('extracts hostname correctly when x-forwarded-host includes port', () => {
57-
const req = makeReq({ 'x-forwarded-proto': 'https', 'x-forwarded-host': 'withport.com:8080' });
58-
const result = getRedirectUri(req, baseEnv);
59-
expect(result).toBe('https://withport.com:3000/callback?storageType=none');
76+
it('extracts hostname correctly from x-forwarded-host with port', () => {
77+
const req = makeReq({
78+
headers: {
79+
'x-forwarded-proto': 'https',
80+
'x-forwarded-host': 'withport.com:9090',
81+
},
6082
});
83+
const result = getRedirectUri(req);
84+
expect(result).toBe('https://withport.com/callback?storageType=none');
85+
});
6186
});

src/auth/redirect-uri.ts

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
11
import type { Request } from 'express';
2-
import {EnvVariables} from "../env/env.service.js";
32

43
/**
54
* Redirection URL is calculated based on the incoming request
65
*/
7-
export const getRedirectUri = (request: Request, env: EnvVariables)=> {
8-
const isStandardOrEmptyPort =
9-
env.frontendPort === '80' ||
10-
env.frontendPort === '443' ||
11-
!env.frontendPort;
12-
const port = isStandardOrEmptyPort ? '' : ':' + env.frontendPort;
6+
export const getRedirectUri = (request: Request) => {
7+
const forwardedPort = request.headers['x-forwarded-port'];
8+
const forwardedPortValue = Array.isArray(forwardedPort)
9+
? forwardedPort[0]
10+
: forwardedPort;
11+
const requestHostPort = request.headers.host?.split(':')[1];
12+
const portFromRequest =
13+
process.env.FRONTEND_PORT || forwardedPortValue || requestHostPort || '';
1314

14-
const forwardedProto = request.headers['x-forwarded-proto'];
15-
const forwardedProtoValue = Array.isArray(forwardedProto)
16-
? forwardedProto[0]
17-
: forwardedProto;
18-
const protocol = forwardedProtoValue || request.protocol;
15+
const isStandardOrEmptyPort =
16+
portFromRequest === '80' || portFromRequest === '443' || !portFromRequest;
17+
const port = isStandardOrEmptyPort ? '' : `:${portFromRequest}`;
1918

20-
const forwardedHost = request.headers['x-forwarded-host'];
21-
const forwardedHostValue = Array.isArray(forwardedHost)
22-
? forwardedHost[0]
23-
: forwardedHost
24-
const forwardedHostname = forwardedHostValue?.split(':')[0];
25-
const host = forwardedHostname || request.hostname;
19+
const forwardedProto = request.headers['x-forwarded-proto'];
20+
const forwardedProtoValue = Array.isArray(forwardedProto)
21+
? forwardedProto[0]
22+
: forwardedProto;
23+
const protocol = forwardedProtoValue || request.protocol;
2624

27-
const redirectionUrl = `${protocol}://${host}${port}`;
28-
return `${redirectionUrl}/callback?storageType=none`;
29-
}
25+
const forwardedHost = request.headers['x-forwarded-host'];
26+
const forwardedHostValue = Array.isArray(forwardedHost)
27+
? forwardedHost[0]
28+
: forwardedHost;
29+
const forwardedHostname = forwardedHostValue?.split(':')[0];
30+
const host = forwardedHostname || request.hostname;
31+
32+
const redirectionUrl = `${protocol}://${host}${port}`;
33+
return `${redirectionUrl}/callback?storageType=none`;
34+
};

src/env/env.controller.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ describe('EnvController', () => {
3737
developmentInstance: false,
3838
isLocal: false,
3939
idpNames: [],
40-
frontendPort: '',
4140
} as EnvConfigVariables;
4241

4342
beforeEach(function () {

src/env/env.service.spec.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,6 @@ describe('EnvService', () => {
8484
delete process.env['ENVIRONMENT'];
8585
});
8686

87-
it('should get empty string for default frontend port', () => {
88-
expect(service.getEnv().frontendPort).toEqual('');
89-
});
90-
91-
it('should get frontend port', () => {
92-
process.env['FRONTEND_PORT'] = '4700';
93-
94-
expect(service.getEnv().frontendPort).toBe('4700');
95-
96-
delete process.env['FRONTEND_PORT'];
97-
});
98-
9987
it('should get if it is a local instance', () => {
10088
process.env['ENVIRONMENT'] = 'local';
10189

src/env/env.service.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export interface EnvVariables extends Record<string, any> {
88
logoutRedirectUrl?: string;
99
healthCheckInterval?: number;
1010
isLocal?: boolean;
11-
frontendPort?: string;
1211
developmentInstance?: boolean;
1312
validWebcomponentUrls?: string[];
1413
uiOptions?: string[];
@@ -25,7 +24,6 @@ export class EnvService {
2524
logoutRedirectUrl: process.env.LOGOUT_REDIRECT_URL || '/logout',
2625
isLocal: process.env.ENVIRONMENT === 'local',
2726
developmentInstance: process.env.DEVELOPMENT_INSTANCE === 'true',
28-
frontendPort: process.env.FRONTEND_PORT || '',
2927
validWebcomponentUrls: (process.env.VALID_WEBCOMPONENT_URLS || '').split(
3028
',',
3129
),

0 commit comments

Comments
 (0)