mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
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:
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}
|
||||
|
||||
Reference in New Issue
Block a user