feat: add email_verified claim

This commit is contained in:
Elias Schneider
2024-10-25 21:33:54 +02:00
parent bd4f87b2d2
commit 5565f60d6d
10 changed files with 64 additions and 29 deletions

View File

@@ -37,7 +37,7 @@ func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) {
"userinfo_endpoint": appUrl + "/api/oidc/userinfo",
"jwks_uri": appUrl + "/.well-known/jwks.json",
"scopes_supported": []string{"openid", "profile", "email"},
"claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "preferred_username"},
"claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "email_verified", "preferred_username"},
"response_types_supported": []string{"code", "id_token"},
"subject_types_supported": []string{"public"},
"id_token_signing_alg_values_supported": []string{"RS256"},

View File

@@ -14,6 +14,7 @@ type AppConfigVariableDto struct {
type AppConfigUpdateDto struct {
AppName string `json:"appName" binding:"required,min=1,max=30"`
SessionDuration string `json:"sessionDuration" binding:"required"`
EmailsVerified string `json:"emailsVerified" binding:"required"`
EmailEnabled string `json:"emailEnabled" binding:"required"`
SmtHost string `json:"smtpHost"`
SmtpPort string `json:"smtpPort"`

View File

@@ -14,6 +14,7 @@ type AppConfig struct {
LogoLightImageType AppConfigVariable
LogoDarkImageType AppConfigVariable
SessionDuration AppConfigVariable
EmailsVerified AppConfigVariable
EmailEnabled AppConfigVariable
SmtpHost AppConfigVariable

View File

@@ -41,6 +41,11 @@ var defaultDbConfig = model.AppConfig{
Type: "number",
Value: "60",
},
EmailsVerified: model.AppConfigVariable{
Key: "emailsVerified",
Type: "bool",
Value: "false",
},
BackgroundImageType: model.AppConfigVariable{
Key: "backgroundImageType",
Type: "string",

View File

@@ -315,6 +315,7 @@ func (s *OidcService) GetUserClaimsForClient(userID string, clientID string) (ma
if strings.Contains(scope, "email") {
claims["email"] = user.Email
claims["email_verified"] = s.appConfigService.DbConfig.EmailsVerified.Value == "true"
}
if strings.Contains(scope, "groups") {

View File

@@ -14,14 +14,19 @@ export default class AppConfigService extends APIService {
const appConfig: Partial<AllAppConfig> = {};
data.forEach(({ key, value }) => {
(appConfig as any)[key] = value;
(appConfig as any)[key] = this.parseValue(value);
});
return appConfig as AllAppConfig;
}
async update(appConfig: AllAppConfig) {
const res = await this.api.put('/application-configuration', appConfig);
// Convert all values to string
const appConfigConvertedToString = {};
for (const key in appConfig) {
(appConfigConvertedToString as any)[key] = (appConfig as any)[key].toString();
}
const res = await this.api.put('/application-configuration', appConfigConvertedToString);
return res.data as AllAppConfig;
}
@@ -62,4 +67,16 @@ export default class AppConfigService extends APIService {
currentVersion
};
}
private parseValue(value: string) {
if (value === 'true') {
return true;
} else if (value === 'false') {
return false;
} else if (!isNaN(Number(value))) {
return Number(value);
} else {
return value;
}
}
}

View File

@@ -1,16 +1,18 @@
export type AllAppConfig = {
export type AppConfig = {
appName: string;
sessionDuration: string;
emailEnabled: string;
};
export type AllAppConfig = AppConfig & {
sessionDuration: number;
emailsVerified: boolean;
emailEnabled: boolean;
smtpHost: string;
smtpPort: string;
smtpPort: number;
smtpFrom: string;
smtpUser: string;
smtpPassword: string;
};
export type AppConfig = AllAppConfig;
export type AppConfigRawResponse = {
key: string;
type: string;
@@ -21,4 +23,4 @@ export type AppVersionInformation = {
isUpToDate: boolean;
newestVersion: string;
currentVersion: string;
};
};

View File

@@ -1,5 +1,5 @@
export function debounced<T extends (...args: any[]) => void>(func: T, delay: number) {
let debounceTimeout: number | undefined;
let debounceTimeout: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
if (debounceTimeout !== undefined) {
@@ -10,4 +10,4 @@ export function debounced<T extends (...args: any[]) => void>(func: T, delay: nu
func(...args);
}, delay);
};
}
}

View File

@@ -15,10 +15,10 @@
} = $props();
let isLoading = $state(false);
let emailEnabled = $state(appConfig.emailEnabled == 'true');
let emailEnabled = $state(appConfig.emailEnabled);
const updatedAppConfig = {
emailEnabled: emailEnabled.toString(),
emailEnabled: appConfig.emailEnabled,
smtpHost: appConfig.smtpHost,
smtpPort: appConfig.smtpPort,
smtpUser: appConfig.smtpUser,
@@ -28,13 +28,13 @@
const formSchema = z.object({
smtpHost: z.string().min(1),
smtpPort: z.string().min(1),
smtpPort: z.number().min(1),
smtpUser: z.string().min(1),
smtpPassword: z.string().min(1),
smtpFrom: z.string().email()
});
const { inputs, ...form } = createForm< typeof formSchema>(formSchema, updatedAppConfig);
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
async function onSubmit() {
const data = form.validate();
@@ -42,15 +42,15 @@
isLoading = true;
await callback({
...data,
emailEnabled: 'true'
emailEnabled: true
}).finally(() => (isLoading = false));
toast.success('Email configuration updated successfully');
return true;
}
async function onDisable() {
await callback({ emailEnabled: 'false' });
emailEnabled = false;
await callback({ emailEnabled });
toast.success('Email disabled successfully');
}
@@ -64,7 +64,7 @@
<form onsubmit={onSubmit}>
<div class="mt-5 grid grid-cols-2 gap-5">
<FormInput label="SMTP Host" bind:input={$inputs.smtpHost} />
<FormInput label="SMTP Port" bind:input={$inputs.smtpPort} />
<FormInput label="SMTP Port" type="number" bind:input={$inputs.smtpPort} />
<FormInput label="SMTP User" bind:input={$inputs.smtpUser} />
<FormInput label="SMTP Password" type="password" bind:input={$inputs.smtpPassword} />
<FormInput label="SMTP From" bind:input={$inputs.smtpFrom} />

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import FormInput from '$lib/components/form-input.svelte';
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Label } from '$lib/components/ui/label';
import type { AllAppConfig } from '$lib/types/application-configuration';
import { createForm } from '$lib/utils/form-util';
import { toast } from 'svelte-sonner';
@@ -18,20 +20,14 @@
const updatedAppConfig = {
appName: appConfig.appName,
sessionDuration: appConfig.sessionDuration
sessionDuration: appConfig.sessionDuration,
emailsVerified: appConfig.emailsVerified
};
const formSchema = z.object({
appName: z.string().min(2).max(30),
sessionDuration: z.string().refine(
(val) => {
const num = Number(val);
return Number.isInteger(num) && num >= 1 && num <= 43200;
},
{
message: 'Session duration must be between 1 and 43200 minutes'
}
)
sessionDuration: z.number().min(1).max(43200),
emailsVerified: z.boolean()
});
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
@@ -49,9 +45,21 @@
<FormInput label="Application Name" bind:input={$inputs.appName} />
<FormInput
label="Session Duration"
type="number"
description="The duration of a session in minutes before the user has to sign in again."
bind:input={$inputs.sessionDuration}
/>
<div class="items-top mt-5 flex space-x-2">
<Checkbox id="admin-privileges" bind:checked={$inputs.emailsVerified.value} />
<div class="grid gap-1.5 leading-none">
<Label for="admin-privileges" class="mb-0 text-sm font-medium leading-none">
Emails Verified
</Label>
<p class="text-muted-foreground text-[0.8rem]">
Whether the user's email should be marked as verified for the OIDC clients.
</p>
</div>
</div>
</div>
<div class="mt-5 flex justify-end">
<Button {isLoading} type="submit">Save</Button>