mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(self-hosting): allow opting in to private-address SSO providers (#1655)
This commit is contained in:
1 parent
cbc0a616ea
commit
578fd61d93
11 files changed
+84
-37
No files matched your search
@@ -364,6 +364,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
auth: {
|
||||
sudoModeSecret: master.auth.sudo_mode_secret,
|
||||
connectionInitiationSecret: master.auth.connection_initiation_secret,
|
||||
ssoAllowPrivateAddresses: master.auth.sso_allow_private_addresses,
|
||||
passkeys: {
|
||||
rpName: master.auth.passkeys.rp_name,
|
||||
rpId: master.auth.passkeys.rp_id,
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from '../../instance/InstanceConfigRepository';
|
||||
import {
|
||||
deriveSsoRedirectUri,
|
||||
getSsoRequestUrlPolicy,
|
||||
isTestSsoProvider,
|
||||
validateSsoPublicOutboundUrl,
|
||||
} from '../../instance/SsoConfigValidation';
|
||||
@@ -653,13 +654,16 @@ export class SsoService {
|
||||
return cached.jwks;
|
||||
}
|
||||
const fetchJwks = async (): Promise<JSONWebKeySet> => {
|
||||
const response = await FetchUtils.sendRequest({
|
||||
url: jwksUrl,
|
||||
method: 'GET',
|
||||
headers: {Accept: 'application/json'},
|
||||
timeout: ms('5 seconds'),
|
||||
serviceName: 'sso_jwks',
|
||||
});
|
||||
const response = await FetchUtils.sendRequest(
|
||||
{
|
||||
url: jwksUrl,
|
||||
method: 'GET',
|
||||
headers: {Accept: 'application/json'},
|
||||
timeout: ms('5 seconds'),
|
||||
serviceName: 'sso_jwks',
|
||||
},
|
||||
{requestUrlPolicy: getSsoRequestUrlPolicy()},
|
||||
);
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new Error(`Failed to fetch JWKS: HTTP ${response.status}`);
|
||||
}
|
||||
@@ -746,16 +750,19 @@ export class SsoService {
|
||||
}
|
||||
|
||||
private async fetchUserInfo(userInfoUrl: string, accessToken: string): Promise<Record<string, unknown>> {
|
||||
const resp = await FetchUtils.sendRequest({
|
||||
url: userInfoUrl,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
const resp = await FetchUtils.sendRequest(
|
||||
{
|
||||
url: userInfoUrl,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
timeout: ms('15 seconds'),
|
||||
serviceName: 'sso_user_info',
|
||||
},
|
||||
timeout: ms('15 seconds'),
|
||||
serviceName: 'sso_user_info',
|
||||
});
|
||||
{requestUrlPolicy: getSsoRequestUrlPolicy()},
|
||||
);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw InputValidationError.fromCode('access_token', ValidationErrorCodes.FAILED_TO_FETCH_SSO_USER_INFO);
|
||||
}
|
||||
@@ -807,14 +814,17 @@ export class SsoService {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
};
|
||||
const resp = await FetchUtils.sendRequest({
|
||||
url: config.tokenUrl ?? '',
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
timeout: ms('15 seconds'),
|
||||
serviceName: 'sso_token_exchange',
|
||||
});
|
||||
const resp = await FetchUtils.sendRequest(
|
||||
{
|
||||
url: config.tokenUrl ?? '',
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
timeout: ms('15 seconds'),
|
||||
serviceName: 'sso_token_exchange',
|
||||
},
|
||||
{requestUrlPolicy: getSsoRequestUrlPolicy()},
|
||||
);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw InputValidationError.fromCode('code', ValidationErrorCodes.INVALID_SSO_AUTHORIZATION_CODE);
|
||||
}
|
||||
|
||||
@@ -259,6 +259,7 @@ export interface APIConfig {
|
||||
auth: {
|
||||
sudoModeSecret: string;
|
||||
connectionInitiationSecret: string;
|
||||
ssoAllowPrivateAddresses: boolean;
|
||||
passkeys: {
|
||||
rpName: string;
|
||||
rpId: string;
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import {domainToASCII} from 'node:url';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
|
||||
import type {RequestUrlPolicy} from '@pkgs/http_client/src/HttpClientTypes';
|
||||
import {createPublicInternetRequestUrlPolicy} from '@pkgs/http_client/src/PublicInternetRequestUrlPolicy';
|
||||
import {Config} from '../Config';
|
||||
|
||||
interface SsoConfigValidationInput {
|
||||
enabled: boolean;
|
||||
@@ -21,7 +23,16 @@ interface NormalizedSsoConfigValidationResult extends SsoConfigValidationInput {
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
const SSO_REQUEST_URL_POLICY = createPublicInternetRequestUrlPolicy();
|
||||
let ssoRequestUrlPolicy: RequestUrlPolicy | null = null;
|
||||
|
||||
export function getSsoRequestUrlPolicy(): RequestUrlPolicy {
|
||||
if (ssoRequestUrlPolicy === null) {
|
||||
ssoRequestUrlPolicy = createPublicInternetRequestUrlPolicy({
|
||||
allowPrivateAddresses: Config.auth.ssoAllowPrivateAddresses,
|
||||
});
|
||||
}
|
||||
return ssoRequestUrlPolicy;
|
||||
}
|
||||
const DOMAIN_LABEL_REGEX = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||
|
||||
function normalizeOptionalSsoString(value: string | null): string | null {
|
||||
@@ -97,7 +108,7 @@ export async function validateSsoPublicOutboundUrl(rawUrl: string, fieldName: st
|
||||
throw InputValidationError.fromCode(fieldName, ValidationErrorCodes.INVALID_URL_FORMAT);
|
||||
}
|
||||
try {
|
||||
await SSO_REQUEST_URL_POLICY.validate(parsedUrl, {
|
||||
await getSsoRequestUrlPolicy().validate(parsedUrl, {
|
||||
phase: 'initial',
|
||||
redirectCount: 0,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createHttpClient} from '@pkgs/http_client/src/HttpClient';
|
||||
import type {HttpClient, RequestOptions, StreamResponse} from '@pkgs/http_client/src/HttpClientTypes';
|
||||
import type {HttpClient, RequestOptions, RequestUrlPolicy, StreamResponse} from '@pkgs/http_client/src/HttpClientTypes';
|
||||
import {createPublicInternetRequestUrlPolicy} from '@pkgs/http_client/src/PublicInternetRequestUrlPolicy';
|
||||
|
||||
const requestUrlPolicy = createPublicInternetRequestUrlPolicy();
|
||||
@@ -9,27 +9,35 @@ const client: HttpClient = createHttpClient({
|
||||
userAgent: 'fluxer-api',
|
||||
requestUrlPolicy,
|
||||
});
|
||||
const redirectScopedClients = new Map<number, HttpClient>();
|
||||
const scopedClients = new Map<RequestUrlPolicy, Map<number, HttpClient>>();
|
||||
|
||||
interface SendRequestOptions {
|
||||
export interface SendRequestOptions {
|
||||
maxRedirects?: number;
|
||||
requestUrlPolicy?: RequestUrlPolicy;
|
||||
}
|
||||
|
||||
function getHttpClientForRequest(options?: SendRequestOptions): HttpClient {
|
||||
if (!options?.maxRedirects) {
|
||||
const policy = options?.requestUrlPolicy ?? requestUrlPolicy;
|
||||
const maxRedirects = options?.maxRedirects ?? 0;
|
||||
if (policy === requestUrlPolicy && maxRedirects === 0) {
|
||||
return client;
|
||||
}
|
||||
const existingClient = redirectScopedClients.get(options.maxRedirects);
|
||||
let clientsForPolicy = scopedClients.get(policy);
|
||||
if (!clientsForPolicy) {
|
||||
clientsForPolicy = new Map<number, HttpClient>();
|
||||
scopedClients.set(policy, clientsForPolicy);
|
||||
}
|
||||
const existingClient = clientsForPolicy.get(maxRedirects);
|
||||
if (existingClient) {
|
||||
return existingClient;
|
||||
}
|
||||
const redirectScopedClient = createHttpClient({
|
||||
const scopedClient = createHttpClient({
|
||||
userAgent: 'fluxer-api',
|
||||
maxRedirects: options.maxRedirects,
|
||||
requestUrlPolicy,
|
||||
...(maxRedirects > 0 ? {maxRedirects} : {}),
|
||||
requestUrlPolicy: policy,
|
||||
});
|
||||
redirectScopedClients.set(options.maxRedirects, redirectScopedClient);
|
||||
return redirectScopedClient;
|
||||
clientsForPolicy.set(maxRedirects, scopedClient);
|
||||
return scopedClient;
|
||||
}
|
||||
|
||||
export async function sendRequest(opts: RequestOptions, options?: SendRequestOptions) {
|
||||
|
||||
Reference in new issue
Block a user