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", "userinfo_endpoint": appUrl + "/api/oidc/userinfo",
"jwks_uri": appUrl + "/.well-known/jwks.json", "jwks_uri": appUrl + "/.well-known/jwks.json",
"scopes_supported": []string{"openid", "profile", "email"}, "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"}, "response_types_supported": []string{"code", "id_token"},
"subject_types_supported": []string{"public"}, "subject_types_supported": []string{"public"},
"id_token_signing_alg_values_supported": []string{"RS256"}, "id_token_signing_alg_values_supported": []string{"RS256"},

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,14 +14,19 @@ export default class AppConfigService extends APIService {
const appConfig: Partial<AllAppConfig> = {}; const appConfig: Partial<AllAppConfig> = {};
data.forEach(({ key, value }) => { data.forEach(({ key, value }) => {
(appConfig as any)[key] = value; (appConfig as any)[key] = this.parseValue(value);
}); });
return appConfig as AllAppConfig; return appConfig as AllAppConfig;
} }
async update(appConfig: 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; return res.data as AllAppConfig;
} }
@@ -62,4 +67,16 @@ export default class AppConfigService extends APIService {
currentVersion 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; appName: string;
sessionDuration: string; };
emailEnabled: string;
export type AllAppConfig = AppConfig & {
sessionDuration: number;
emailsVerified: boolean;
emailEnabled: boolean;
smtpHost: string; smtpHost: string;
smtpPort: string; smtpPort: number;
smtpFrom: string; smtpFrom: string;
smtpUser: string; smtpUser: string;
smtpPassword: string; smtpPassword: string;
}; };
export type AppConfig = AllAppConfig;
export type AppConfigRawResponse = { export type AppConfigRawResponse = {
key: string; key: string;
type: string; type: string;
@@ -21,4 +23,4 @@ export type AppVersionInformation = {
isUpToDate: boolean; isUpToDate: boolean;
newestVersion: string; newestVersion: string;
currentVersion: string; currentVersion: string;
}; };

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import FormInput from '$lib/components/form-input.svelte'; import FormInput from '$lib/components/form-input.svelte';
import { Button } from '$lib/components/ui/button'; 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 type { AllAppConfig } from '$lib/types/application-configuration';
import { createForm } from '$lib/utils/form-util'; import { createForm } from '$lib/utils/form-util';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@@ -18,20 +20,14 @@
const updatedAppConfig = { const updatedAppConfig = {
appName: appConfig.appName, appName: appConfig.appName,
sessionDuration: appConfig.sessionDuration sessionDuration: appConfig.sessionDuration,
emailsVerified: appConfig.emailsVerified
}; };
const formSchema = z.object({ const formSchema = z.object({
appName: z.string().min(2).max(30), appName: z.string().min(2).max(30),
sessionDuration: z.string().refine( sessionDuration: z.number().min(1).max(43200),
(val) => { emailsVerified: z.boolean()
const num = Number(val);
return Number.isInteger(num) && num >= 1 && num <= 43200;
},
{
message: 'Session duration must be between 1 and 43200 minutes'
}
)
}); });
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig); const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
@@ -49,9 +45,21 @@
<FormInput label="Application Name" bind:input={$inputs.appName} /> <FormInput label="Application Name" bind:input={$inputs.appName} />
<FormInput <FormInput
label="Session Duration" label="Session Duration"
type="number"
description="The duration of a session in minutes before the user has to sign in again." description="The duration of a session in minutes before the user has to sign in again."
bind:input={$inputs.sessionDuration} 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>
<div class="mt-5 flex justify-end"> <div class="mt-5 flex justify-end">
<Button {isLoading} type="submit">Save</Button> <Button {isLoading} type="submit">Save</Button>