fix!: do not allow insecure oauth requests by default (#27844)

* fix!: do not allow insecure oauth requests by default

* fix: format

* fix: make open-api

* fix: tests

* nit: casing

* chore: migration to allow insecure if current oauth uses http
This commit is contained in:
bo0tzz
2026-04-16 16:11:58 +02:00
committed by GitHub
parent 9c642bd6fc
commit 3356e81c85
11 changed files with 77 additions and 3 deletions
@@ -76,6 +76,7 @@ const setupOAuth = async (token: string, dto: Partial<SystemConfigOAuthDto>) =>
...defaults.oauth,
buttonText: 'Login with Immich',
issuerUrl: `${authServer.internal}/.well-known/openid-configuration`,
allowInsecureRequests: true,
...dto,
};
await updateConfig({ systemConfigDto: { ...defaults, oauth: merged } }, options);
@@ -399,4 +400,23 @@ describe(`/oauth`, () => {
});
});
});
describe('allowInsecureRequests: false', () => {
beforeAll(async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
allowInsecureRequests: false,
});
});
it('should reject OAuth discovery over HTTP', async () => {
const { status, body } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(500);
expect(body).toMatchObject({ statusCode: 500 });
});
});
});
+2
View File
@@ -267,6 +267,8 @@
"notification_enable_email_notifications": "Enable email notifications",
"notification_settings": "Notification Settings",
"notification_settings_description": "Manage notification settings, including email",
"oauth_allow_insecure_requests": "Allow insecure requests",
"oauth_allow_insecure_requests_description": "WARNING: This disables TLS certificate validation for OAuth requests and may expose you to MITM attacks.",
"oauth_auto_launch": "Auto launch",
"oauth_auto_launch_description": "Start the OAuth login flow automatically upon navigating to the login page",
"oauth_auto_register": "Auto register",
+10 -1
View File
@@ -13,6 +13,7 @@ part of openapi.api;
class SystemConfigOAuthDto {
/// Returns a new [SystemConfigOAuthDto] instance.
SystemConfigOAuthDto({
required this.allowInsecureRequests,
required this.autoLaunch,
required this.autoRegister,
required this.buttonText,
@@ -33,6 +34,9 @@ class SystemConfigOAuthDto {
required this.tokenEndpointAuthMethod,
});
/// Allow insecure requests
bool allowInsecureRequests;
/// Auto launch
bool autoLaunch;
@@ -93,6 +97,7 @@ class SystemConfigOAuthDto {
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigOAuthDto &&
other.allowInsecureRequests == allowInsecureRequests &&
other.autoLaunch == autoLaunch &&
other.autoRegister == autoRegister &&
other.buttonText == buttonText &&
@@ -115,6 +120,7 @@ class SystemConfigOAuthDto {
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(allowInsecureRequests.hashCode) +
(autoLaunch.hashCode) +
(autoRegister.hashCode) +
(buttonText.hashCode) +
@@ -135,10 +141,11 @@ class SystemConfigOAuthDto {
(tokenEndpointAuthMethod.hashCode);
@override
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
String toString() => 'SystemConfigOAuthDto[allowInsecureRequests=$allowInsecureRequests, autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'allowInsecureRequests'] = this.allowInsecureRequests;
json[r'autoLaunch'] = this.autoLaunch;
json[r'autoRegister'] = this.autoRegister;
json[r'buttonText'] = this.buttonText;
@@ -173,6 +180,7 @@ class SystemConfigOAuthDto {
final json = value.cast<String, dynamic>();
return SystemConfigOAuthDto(
allowInsecureRequests: mapValueOfType<bool>(json, r'allowInsecureRequests')!,
autoLaunch: mapValueOfType<bool>(json, r'autoLaunch')!,
autoRegister: mapValueOfType<bool>(json, r'autoRegister')!,
buttonText: mapValueOfType<String>(json, r'buttonText')!,
@@ -240,6 +248,7 @@ class SystemConfigOAuthDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'allowInsecureRequests',
'autoLaunch',
'autoRegister',
'buttonText',
+5
View File
@@ -24302,6 +24302,10 @@
},
"SystemConfigOAuthDto": {
"properties": {
"allowInsecureRequests": {
"description": "Allow insecure requests",
"type": "boolean"
},
"autoLaunch": {
"description": "Auto launch",
"type": "boolean"
@@ -24379,6 +24383,7 @@
}
},
"required": [
"allowInsecureRequests",
"autoLaunch",
"autoRegister",
"buttonText",
@@ -2502,6 +2502,8 @@ export type SystemConfigNotificationsDto = {
smtp: SystemConfigSmtpDto;
};
export type SystemConfigOAuthDto = {
/** Allow insecure requests */
allowInsecureRequests: boolean;
/** Auto launch */
autoLaunch: boolean;
/** Auto register */
+2
View File
@@ -111,6 +111,7 @@ export type SystemConfig = {
profileSigningAlgorithm: string;
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod;
timeout: number;
allowInsecureRequests: boolean;
storageLabelClaim: string;
storageQuotaClaim: string;
roleClaim: string;
@@ -305,6 +306,7 @@ export const defaults = Object.freeze<SystemConfig>({
roleClaim: 'immich_role',
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.ClientSecretPost,
timeout: 30_000,
allowInsecureRequests: false,
},
passwordLogin: {
enabled: true,
+1
View File
@@ -179,6 +179,7 @@ const SystemConfigOAuthSchema = z
clientSecret: z.string().describe('Client secret'),
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema,
timeout: z.int().min(1).describe('Timeout'),
allowInsecureRequests: configBool.describe('Allow insecure requests'),
defaultStorageQuota: z.number().min(0).nullable().describe('Default storage quota'),
enabled: configBool.describe('Enabled'),
issuerUrl: z
+4 -2
View File
@@ -1,6 +1,6 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import {
allowInsecureRequests,
allowInsecureRequests as allowInsecureRequestsExecute,
authorizationCodeGrant,
buildAuthorizationUrl,
calculatePKCECodeChallenge,
@@ -28,6 +28,7 @@ export type OAuthConfig = {
signingAlgorithm: string;
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod;
timeout: number;
allowInsecureRequests: boolean;
};
export type OAuthProfile = UserInfoResponse;
@@ -133,6 +134,7 @@ export class OAuthRepository {
signingAlgorithm,
tokenEndpointAuthMethod,
timeout,
allowInsecureRequests,
}: OAuthConfig) {
try {
return await discovery(
@@ -146,7 +148,7 @@ export class OAuthRepository {
},
this.getTokenAuthMethod(tokenEndpointAuthMethod, clientSecret),
{
execute: [allowInsecureRequests],
execute: allowInsecureRequests ? [allowInsecureRequestsExecute] : [],
timeout,
},
);
@@ -0,0 +1,22 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`
UPDATE system_metadata
SET value = jsonb_set(
value,
'{oauth,allowInsecureRequests}',
'true'::jsonb
)
WHERE key = 'system-config'
AND value->'oauth'->>'issuerUrl' LIKE 'http://%'
`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`
UPDATE system_metadata
SET value = value #- '{oauth,allowInsecureRequests}'
WHERE key = 'system-config'
`.execute(db);
}
@@ -145,6 +145,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
profileSigningAlgorithm: 'none',
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.ClientSecretPost,
timeout: 30_000,
allowInsecureRequests: false,
storageLabelClaim: 'preferred_username',
storageQuotaClaim: 'immich_quota',
roleClaim: 'immich_role',
@@ -174,6 +174,14 @@
isEdited={!(configToEdit.oauth.timeout === config.oauth.timeout)}
/>
<SettingSwitch
title={$t('admin.oauth_allow_insecure_requests')}
subtitle={$t('admin.oauth_allow_insecure_requests_description')}
bind:checked={configToEdit.oauth.allowInsecureRequests}
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.allowInsecureRequests === config.oauth.allowInsecureRequests)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_storage_label_claim')}