feat: add support for multiple callback urls

This commit is contained in:
Elias Schneider
2024-08-24 00:49:08 +02:00
parent ae7aeb0945
commit 8166e2ead7
20 changed files with 287 additions and 101 deletions

View File

@@ -6,7 +6,6 @@ var (
ErrUsernameTaken = errors.New("username is already taken") ErrUsernameTaken = errors.New("username is already taken")
ErrEmailTaken = errors.New("email is already taken") ErrEmailTaken = errors.New("email is already taken")
ErrSetupAlreadyCompleted = errors.New("setup already completed") ErrSetupAlreadyCompleted = errors.New("setup already completed")
ErrInvalidBody = errors.New("invalid request body")
ErrTokenInvalidOrExpired = errors.New("token is invalid or expired") ErrTokenInvalidOrExpired = errors.New("token is invalid or expired")
ErrOidcMissingAuthorization = errors.New("missing authorization") ErrOidcMissingAuthorization = errors.New("missing authorization")
ErrOidcGrantTypeNotSupported = errors.New("grant type not supported") ErrOidcGrantTypeNotSupported = errors.New("grant type not supported")

View File

@@ -40,39 +40,55 @@ type OidcController struct {
} }
func (oc *OidcController) authorizeHandler(c *gin.Context) { func (oc *OidcController) authorizeHandler(c *gin.Context) {
var input dto.AuthorizeOidcClientDto var input dto.AuthorizeOidcClientRequestDto
if err := c.ShouldBindJSON(&input); err != nil { if err := c.ShouldBindJSON(&input); err != nil {
utils.ControllerError(c, err) utils.ControllerError(c, err)
return return
} }
code, err := oc.oidcService.Authorize(input, c.GetString("userID")) code, callbackURL, err := oc.oidcService.Authorize(input, c.GetString("userID"))
if err != nil { if err != nil {
if errors.Is(err, common.ErrOidcMissingAuthorization) { if errors.Is(err, common.ErrOidcMissingAuthorization) {
utils.CustomControllerError(c, http.StatusForbidden, err.Error()) utils.CustomControllerError(c, http.StatusForbidden, err.Error())
} else if errors.Is(err, common.ErrOidcInvalidCallbackURL) {
utils.CustomControllerError(c, http.StatusBadRequest, err.Error())
} else { } else {
utils.ControllerError(c, err) utils.ControllerError(c, err)
} }
return return
} }
c.JSON(http.StatusOK, gin.H{"code": code}) response := dto.AuthorizeOidcClientResponseDto{
Code: code,
CallbackURL: callbackURL,
}
c.JSON(http.StatusOK, response)
} }
func (oc *OidcController) authorizeNewClientHandler(c *gin.Context) { func (oc *OidcController) authorizeNewClientHandler(c *gin.Context) {
var input dto.AuthorizeOidcClientDto var input dto.AuthorizeOidcClientRequestDto
if err := c.ShouldBindJSON(&input); err != nil { if err := c.ShouldBindJSON(&input); err != nil {
utils.ControllerError(c, err) utils.ControllerError(c, err)
return return
} }
code, err := oc.oidcService.AuthorizeNewClient(input, c.GetString("userID")) code, callbackURL, err := oc.oidcService.AuthorizeNewClient(input, c.GetString("userID"))
if err != nil { if err != nil {
utils.ControllerError(c, err) if errors.Is(err, common.ErrOidcInvalidCallbackURL) {
utils.CustomControllerError(c, http.StatusBadRequest, err.Error())
} else {
utils.ControllerError(c, err)
}
return return
} }
c.JSON(http.StatusOK, gin.H{"code": code}) response := dto.AuthorizeOidcClientResponseDto{
Code: code,
CallbackURL: callbackURL,
}
c.JSON(http.StatusOK, response)
} }
func (oc *OidcController) createIDTokenHandler(c *gin.Context) { func (oc *OidcController) createIDTokenHandler(c *gin.Context) {

View File

@@ -17,10 +17,16 @@ type OidcClientCreateDto struct {
CallbackURLs []string `json:"callbackURLs" binding:"required,urlList"` CallbackURLs []string `json:"callbackURLs" binding:"required,urlList"`
} }
type AuthorizeOidcClientDto struct { type AuthorizeOidcClientRequestDto struct {
ClientID string `json:"clientID" binding:"required"` ClientID string `json:"clientID" binding:"required"`
Scope string `json:"scope" binding:"required"` Scope string `json:"scope" binding:"required"`
Nonce string `json:"nonce"` CallbackURL string `json:"callbackURL"`
Nonce string `json:"nonce"`
}
type AuthorizeOidcClientResponseDto struct {
Code string `json:"code"`
CallbackURL string `json:"callbackURL"`
} }
type OidcIdTokenDto struct { type OidcIdTokenDto struct {

View File

@@ -52,17 +52,14 @@ func (c *OidcClient) AfterFind(_ *gorm.DB) (err error) {
type CallbackURLs []string type CallbackURLs []string
func (s *CallbackURLs) Scan(value interface{}) error { func (cu *CallbackURLs) Scan(value interface{}) error {
switch v := value.(type) { if v, ok := value.([]byte); ok {
case []byte: return json.Unmarshal(v, cu)
return json.Unmarshal(v, s) } else {
case string: return errors.New("type assertion to []byte failed")
return json.Unmarshal([]byte(v), s)
default:
return errors.New("type assertion to []byte or string failed")
} }
} }
func (atl CallbackURLs) Value() (driver.Value, error) { func (cu CallbackURLs) Value() (driver.Value, error) {
return json.Marshal(atl) return json.Marshal(cu)
} }

View File

@@ -11,6 +11,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"mime/multipart" "mime/multipart"
"os" "os"
"slices"
"strings" "strings"
"time" "time"
) )
@@ -27,33 +28,50 @@ func NewOidcService(db *gorm.DB, jwtService *JwtService) *OidcService {
} }
} }
func (s *OidcService) Authorize(req dto.AuthorizeOidcClientDto, userID string) (string, error) { func (s *OidcService) Authorize(input dto.AuthorizeOidcClientRequestDto, userID string) (string, string, error) {
var userAuthorizedOIDCClient model.UserAuthorizedOidcClient var userAuthorizedOIDCClient model.UserAuthorizedOidcClient
s.db.First(&userAuthorizedOIDCClient, "client_id = ? AND user_id = ?", req.ClientID, userID) s.db.Preload("Client").First(&userAuthorizedOIDCClient, "client_id = ? AND user_id = ?", input.ClientID, userID)
if userAuthorizedOIDCClient.Scope != req.Scope { if userAuthorizedOIDCClient.Scope != input.Scope {
return "", common.ErrOidcMissingAuthorization return "", "", common.ErrOidcMissingAuthorization
} }
return s.createAuthorizationCode(req.ClientID, userID, req.Scope, req.Nonce) callbackURL, err := getCallbackURL(userAuthorizedOIDCClient.Client, input.CallbackURL)
if err != nil {
return "", "", err
}
code, err := s.createAuthorizationCode(input.ClientID, userID, input.Scope, input.Nonce)
return code, callbackURL, err
} }
func (s *OidcService) AuthorizeNewClient(req dto.AuthorizeOidcClientDto, userID string) (string, error) { func (s *OidcService) AuthorizeNewClient(input dto.AuthorizeOidcClientRequestDto, userID string) (string, string, error) {
var client model.OidcClient
if err := s.db.First(&client, "id = ?", input.ClientID).Error; err != nil {
return "", "", err
}
callbackURL, err := getCallbackURL(client, input.CallbackURL)
if err != nil {
return "", "", err
}
userAuthorizedClient := model.UserAuthorizedOidcClient{ userAuthorizedClient := model.UserAuthorizedOidcClient{
UserID: userID, UserID: userID,
ClientID: req.ClientID, ClientID: input.ClientID,
Scope: req.Scope, Scope: input.Scope,
} }
if err := s.db.Create(&userAuthorizedClient).Error; err != nil { if err := s.db.Create(&userAuthorizedClient).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) { if errors.Is(err, gorm.ErrDuplicatedKey) {
err = s.db.Model(&userAuthorizedClient).Update("scope", req.Scope).Error err = s.db.Model(&userAuthorizedClient).Update("scope", input.Scope).Error
} else { } else {
return "", err return "", "", err
} }
} }
return s.createAuthorizationCode(req.ClientID, userID, req.Scope, req.Nonce) code, err := s.createAuthorizationCode(input.ClientID, userID, input.Scope, input.Nonce)
return code, callbackURL, err
} }
func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret string) (string, string, error) { func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret string) (string, string, error) {
@@ -321,3 +339,14 @@ func (s *OidcService) createAuthorizationCode(clientID string, userID string, sc
return randomString, nil return randomString, nil
} }
func getCallbackURL(client model.OidcClient, inputCallbackURL string) (callbackURL string, err error) {
if inputCallbackURL == "" {
return client.CallbackURLs[0], nil
}
if slices.Contains(client.CallbackURLs, inputCallbackURL) {
return inputCallbackURL, nil
}
return "", common.ErrOidcInvalidCallbackURL
}

View File

@@ -58,7 +58,7 @@ func handleValidationError(validationErrors validator.ValidationErrors) string {
default: default:
errorMessage = fmt.Sprintf("%s is invalid", fieldName) errorMessage = fmt.Sprintf("%s is invalid", fieldName)
} }
errorMessages = append(errorMessages, errorMessage) errorMessages = append(errorMessages, errorMessage)
} }

View File

@@ -57,7 +57,7 @@ CREATE TABLE webauthn_credentials
credential_id TEXT NOT NULL UNIQUE, credential_id TEXT NOT NULL UNIQUE,
public_key BLOB NOT NULL, public_key BLOB NOT NULL,
attestation_type TEXT NOT NULL, attestation_type TEXT NOT NULL,
transport TEXT NOT NULL, transport BLOB NOT NULL,
user_id TEXT REFERENCES users user_id TEXT REFERENCES users
); );

View File

@@ -0,0 +1,25 @@
create table oidc_clients
(
id TEXT not null
primary key,
created_at DATETIME,
name TEXT,
secret TEXT,
callback_url TEXT,
image_type TEXT,
created_by_id TEXT
references users
);
insert into oidc_clients(id, created_at, name, secret, callback_url, image_type, created_by_id)
select
id,
created_at,
name,
secret,
json_extract(callback_urls, '$[0]'),
image_type,
created_by_id
from oidc_clients_dg_tmp;
drop table oidc_clients_dg_tmp;

View File

@@ -0,0 +1,27 @@
create table oidc_clients_dg_tmp
(
id TEXT not null
primary key,
created_at DATETIME,
name TEXT,
secret TEXT,
callback_urls BLOB,
image_type TEXT,
created_by_id TEXT
references users
);
insert into oidc_clients_dg_tmp(id, created_at, name, secret, callback_urls, image_type, created_by_id)
select id,
created_at,
name,
secret,
CAST(json_group_array(json_quote(callback_url)) AS BLOB),
image_type,
created_by_id
from oidc_clients;
drop table oidc_clients;
alter table oidc_clients_dg_tmp
rename to oidc_clients;

View File

@@ -2,15 +2,17 @@
import { Label } from '$lib/components/ui/label'; import { Label } from '$lib/components/ui/label';
import type { FormInput } from '$lib/utils/form-util'; import type { FormInput } from '$lib/utils/form-util';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { Input } from './ui/input'; import { Input } from './ui/input';
let { let {
input = $bindable(), input = $bindable(),
label, label,
description, description,
children children,
}: { ...restProps
input: FormInput<string | boolean | number>; }: HTMLAttributes<HTMLDivElement> & {
input?: FormInput<string | boolean | number>;
label: string; label: string;
description?: string; description?: string;
children?: Snippet; children?: Snippet;
@@ -19,19 +21,19 @@
const id = label.toLowerCase().replace(/ /g, '-'); const id = label.toLowerCase().replace(/ /g, '-');
</script> </script>
<div> <div {...restProps}>
<Label class="mb-0" for={id}>{label}</Label> <Label class="mb-0" for={id}>{label}</Label>
{#if description} {#if description}
<p class="text-muted-foreground text-xs mt-1">{description}</p> <p class="text-muted-foreground mt-1 text-xs">{description}</p>
{/if} {/if}
<div class="mt-2"> <div class="mt-2">
{#if children} {#if children}
{@render children()} {@render children()}
{:else} {:else if input}
<Input {id} bind:value={input.value} /> <Input {id} bind:value={input.value} />
{/if} {/if}
{#if input.error} {#if input?.error}
<p class="text-sm text-red-500">{input.error}</p> <p class="mt-1 text-sm text-red-500">{input.error}</p>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -1,26 +1,28 @@
import type { OidcClient, OidcClientCreate } from '$lib/types/oidc.type'; import type { AuthorizeResponse, OidcClient, OidcClientCreate } from '$lib/types/oidc.type';
import type { Paginated, PaginationRequest } from '$lib/types/pagination.type'; import type { Paginated, PaginationRequest } from '$lib/types/pagination.type';
import APIService from './api-service'; import APIService from './api-service';
class OidcService extends APIService { class OidcService extends APIService {
async authorize(clientId: string, scope: string, nonce?: string) { async authorize(clientId: string, scope: string, callbackURL : string, nonce?: string) {
const res = await this.api.post('/oidc/authorize', { const res = await this.api.post('/oidc/authorize', {
scope, scope,
nonce, nonce,
callbackURL,
clientId clientId
}); });
return res.data.code as string; return res.data as AuthorizeResponse;
} }
async authorizeNewClient(clientId: string, scope: string, nonce?: string) { async authorizeNewClient(clientId: string, scope: string, callbackURL: string, nonce?: string) {
const res = await this.api.post('/oidc/authorize/new-client', { const res = await this.api.post('/oidc/authorize/new-client', {
scope, scope,
nonce, nonce,
callbackURL,
clientId clientId
}); });
return res.data.code as string; return res.data as AuthorizeResponse;
} }
async listClients(search?: string, pagination?: PaginationRequest) { async listClients(search?: string, pagination?: PaginationRequest) {

View File

@@ -2,7 +2,7 @@ export type OidcClient = {
id: string; id: string;
name: string; name: string;
logoURL: string; logoURL: string;
callbackURL: string; callbackURLs: [string, ...string[]];
hasLogo: boolean; hasLogo: boolean;
}; };
@@ -11,3 +11,8 @@ export type OidcClientCreate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
export type OidcClientCreateWithLogo = OidcClientCreate & { export type OidcClientCreateWithLogo = OidcClientCreate & {
logo: File | null; logo: File | null;
}; };
export type AuthorizeResponse = {
code: string;
callbackURL: string;
};

View File

@@ -11,6 +11,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
scope: url.searchParams.get('scope')!, scope: url.searchParams.get('scope')!,
nonce: url.searchParams.get('nonce') || undefined, nonce: url.searchParams.get('nonce') || undefined,
state: url.searchParams.get('state')!, state: url.searchParams.get('state')!,
callbackURL: url.searchParams.get('redirect_uri')!,
client client
}; };
}; };

View File

@@ -24,7 +24,7 @@
let authorizationRequired = false; let authorizationRequired = false;
export let data: PageData; export let data: PageData;
let { scope, nonce, client, state } = data; let { scope, nonce, client, state, callbackURL } = data;
async function authorize() { async function authorize() {
isLoading = true; isLoading = true;
@@ -36,9 +36,11 @@
await webauthnService.finishLogin(authResponse); await webauthnService.finishLogin(authResponse);
} }
await oidService.authorize(client!.id, scope, nonce).then(async (code) => { await oidService
onSuccess(code); .authorize(client!.id, scope, callbackURL, nonce)
}); .then(async ({ code, callbackURL }) => {
onSuccess(code, callbackURL);
});
} catch (e) { } catch (e) {
if (e instanceof AxiosError && e.response?.status === 403) { if (e instanceof AxiosError && e.response?.status === 403) {
authorizationRequired = true; authorizationRequired = true;
@@ -52,19 +54,21 @@
async function authorizeNewClient() { async function authorizeNewClient() {
isLoading = true; isLoading = true;
try { try {
await oidService.authorizeNewClient(client!.id, scope, nonce).then(async (code) => { await oidService
onSuccess(code); .authorizeNewClient(client!.id, scope, callbackURL, nonce)
}); .then(async ({ code, callbackURL }) => {
onSuccess(code, callbackURL);
});
} catch (e) { } catch (e) {
errorMessage = getWebauthnErrorMessage(e); errorMessage = getWebauthnErrorMessage(e);
isLoading = false; isLoading = false;
} }
} }
function onSuccess(code: string) { function onSuccess(code: string, callbackURL: string) {
success = true; success = true;
setTimeout(() => { setTimeout(() => {
window.location.href = `${client!.callbackURL}?code=${code}&state=${state}`; window.location.href = `${callbackURL}?code=${code}&state=${state}`;
}, 1000); }, 1000);
} }
</script> </script>

View File

@@ -47,7 +47,6 @@
<form onsubmit={onSubmit}> <form onsubmit={onSubmit}>
<div class="flex flex-col gap-5"> <div class="flex flex-col gap-5">
<FormInput label="Application Name" bind:input={$inputs.appName} /> <FormInput label="Application Name" bind:input={$inputs.appName} />
<FormInput <FormInput
label="Session Duration" label="Session Duration"
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."

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import FormInput from '$lib/components/form-input.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { LucideMinus, LucidePlus } from 'lucide-svelte';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
let {
callbackURLs = $bindable(),
error = $bindable(null),
...restProps
}: HTMLAttributes<HTMLDivElement> & {
callbackURLs: string[];
error?: string | null;
children?: Snippet;
} = $props();
const limit = 5;
</script>
<div {...restProps}>
<FormInput label="Callback URLs">
<div class="flex flex-col gap-y-2">
{#each callbackURLs as _, i}
<div class="flex gap-x-2">
<Input data-testid={`callback-url-${i + 1}`} bind:value={callbackURLs[i]} />
{#if callbackURLs.length > 1}
<Button
variant="outline"
size="sm"
on:click={() => callbackURLs = callbackURLs.filter((_, index) => index !== i)}
>
<LucideMinus class="h-4 w-4" />
</Button>
{/if}
</div>
{/each}
</div>
</FormInput>
{#if error}
<p class="mt-1 text-sm text-red-500">{error}</p>
{/if}
{#if callbackURLs.length < limit}
<Button
class="mt-2"
variant="secondary"
size="sm"
on:click={() => callbackURLs = [...callbackURLs, '']}
>
<LucidePlus class="mr-1 h-4 w-4" />
Add another
</Button>
{/if}
</div>

View File

@@ -10,6 +10,7 @@
} from '$lib/types/oidc.type'; } from '$lib/types/oidc.type';
import { createForm } from '$lib/utils/form-util'; import { createForm } from '$lib/utils/form-util';
import { z } from 'zod'; import { z } from 'zod';
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
let { let {
callback, callback,
@@ -27,12 +28,12 @@
const client: OidcClientCreate = { const client: OidcClientCreate = {
name: existingClient?.name || '', name: existingClient?.name || '',
callbackURL: existingClient?.callbackURL || '' callbackURLs: existingClient?.callbackURLs || [""]
}; };
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(2).max(50), name: z.string().min(2).max(50),
callbackURL: z.string().url() callbackURLs: z.array(z.string().url()).nonempty()
}); });
type FormSchema = typeof formSchema; type FormSchema = typeof formSchema;
@@ -70,32 +71,40 @@
</script> </script>
<form onsubmit={onSubmit}> <form onsubmit={onSubmit}>
<div class="mt-3 grid grid-cols-2 gap-3"> <div class="flex flex-col gap-3 sm:flex-row">
<FormInput label="Name" bind:input={$inputs.name} /> <FormInput label="Name" class="w-full" bind:input={$inputs.name} />
<FormInput label="Callback URL" bind:input={$inputs.callbackURL} /> <OidcCallbackUrlInput
<div class="mt-3"> class="w-full"
<Label for="logo">Logo</Label> bind:callbackURLs={$inputs.callbackURLs.value}
<div class="mt-2 flex items-end gap-3"> bind:error={$inputs.callbackURLs.error}
{#if logoDataURL} />
<div class="h-32 w-32 rounded-2xl bg-muted p-3"> </div>
<img class="m-auto max-h-full max-w-full object-contain" src={logoDataURL} alt={`${$inputs.name.value} logo`} /> <div class="mt-3">
</div> <Label for="logo">Logo</Label>
{/if} <div class="mt-2 flex items-end gap-3">
<div class="flex flex-col gap-2"> {#if logoDataURL}
<FileInput <div class="bg-muted h-32 w-32 rounded-2xl p-3">
id="logo" <img
variant="secondary" class="m-auto max-h-full max-w-full object-contain"
accept="image/png, image/jpeg, image/svg+xml" src={logoDataURL}
onchange={onLogoChange} alt={`${$inputs.name.value} logo`}
> />
<Button variant="secondary">
{existingClient?.hasLogo ? 'Change Logo' : 'Upload Logo'}
</Button>
</FileInput>
{#if logoDataURL}
<Button variant="outline" on:click={resetLogo}>Remove Logo</Button>
{/if}
</div> </div>
{/if}
<div class="flex flex-col gap-2">
<FileInput
id="logo"
variant="secondary"
accept="image/png, image/jpeg, image/svg+xml"
onchange={onLogoChange}
>
<Button variant="secondary">
{existingClient?.hasLogo ? 'Change Logo' : 'Upload Logo'}
</Button>
</FileInput>
{#if logoDataURL}
<Button variant="outline" on:click={resetLogo}>Remove Logo</Button>
{/if}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -26,9 +26,13 @@
}; };
const formSchema = z.object({ const formSchema = z.object({
firstName: z.string().min(2).max(50), firstName: z.string().min(2).max(30),
lastName: z.string().min(2).max(50), lastName: z.string().min(2).max(30),
username: z.string().min(2).max(50), username: z
.string()
.min(2)
.max(30)
.regex(/^[a-z0-9_]+$/, 'Only lowercase letters, numbers, and underscores are allowed'),
email: z.string().email(), email: z.string().email(),
isAdmin: z.boolean() isAdmin: z.boolean()
}); });
@@ -66,10 +70,10 @@
<div class="items-top mt-5 flex space-x-2"> <div class="items-top mt-5 flex space-x-2">
<Checkbox id="admin-privileges" bind:checked={$inputs.isAdmin.value} /> <Checkbox id="admin-privileges" bind:checked={$inputs.isAdmin.value} />
<div class="grid gap-1.5 leading-none"> <div class="grid gap-1.5 leading-none">
<Label for="admin-privileges" class="text-sm font-medium leading-none mb-0"> <Label for="admin-privileges" class="mb-0 text-sm font-medium leading-none">
Admin Privileges Admin Privileges
</Label> </Label>
<p class="text-[0.8rem] text-muted-foreground">Admins have full access to the admin panel.</p> <p class="text-muted-foreground text-[0.8rem]">Admins have full access to the admin panel.</p>
</div> </div>
</div> </div>
<div class="mt-5 flex justify-end"> <div class="mt-5 flex justify-end">

View File

@@ -23,17 +23,18 @@ export const users = {
export const oidcClients = { export const oidcClients = {
nextcloud: { nextcloud: {
id: "3654a746-35d4-4321-ac61-0bdcff2b4055", id: '3654a746-35d4-4321-ac61-0bdcff2b4055',
name: 'Nextcloud', name: 'Nextcloud',
callbackUrl: 'http://nextcloud/auth/callback' callbackUrl: 'http://nextcloud/auth/callback'
}, },
immich: { immich: {
id: "606c7782-f2b1-49e5-8ea9-26eb1b06d018", id: '606c7782-f2b1-49e5-8ea9-26eb1b06d018',
name: 'Immich', name: 'Immich',
callbackUrl: 'http://immich/auth/callback' callbackUrl: 'http://immich/auth/callback'
}, },
pingvinShare: { pingvinShare: {
name: 'Pingvin Share', name: 'Pingvin Share',
callbackUrl: 'http://pingvin.share/auth/callback' callbackUrl: 'http://pingvin.share/auth/callback',
secondCallbackUrl: 'http://pingvin.share/auth/callback2'
} }
}; };

View File

@@ -10,7 +10,11 @@ test('Create OIDC client', async ({ page }) => {
await page.getByRole('button', { name: 'Add OIDC Client' }).click(); await page.getByRole('button', { name: 'Add OIDC Client' }).click();
await page.getByLabel('Name').fill(oidcClient.name); await page.getByLabel('Name').fill(oidcClient.name);
await page.getByLabel('Callback URL').fill(oidcClient.callbackUrl);
await page.getByTestId('callback-url-1').fill(oidcClient.callbackUrl);
await page.getByRole('button', { name: 'Add another' }).click();
await page.getByTestId('callback-url-2').fill(oidcClient.secondCallbackUrl!);
await page.getByLabel('logo').setInputFiles('tests/assets/pingvin-share-logo.png'); await page.getByLabel('logo').setInputFiles('tests/assets/pingvin-share-logo.png');
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
@@ -20,7 +24,8 @@ test('Create OIDC client', async ({ page }) => {
expect(clientId?.length).toBe(36); expect(clientId?.length).toBe(36);
expect((await page.getByTestId('client-secret').textContent())?.length).toBe(32); expect((await page.getByTestId('client-secret').textContent())?.length).toBe(32);
await expect(page.getByLabel('Name')).toHaveValue(oidcClient.name); await expect(page.getByLabel('Name')).toHaveValue(oidcClient.name);
await expect(page.getByLabel('Callback URL')).toHaveValue(oidcClient.callbackUrl); await expect(page.getByTestId('callback-url-1')).toHaveValue(oidcClient.callbackUrl);
await expect(page.getByTestId('callback-url-2')).toHaveValue(oidcClient.secondCallbackUrl!);
await expect(page.getByRole('img', { name: `${oidcClient.name} logo` })).toBeVisible(); await expect(page.getByRole('img', { name: `${oidcClient.name} logo` })).toBeVisible();
await page.request await page.request
.get(`/api/oidc/clients/${clientId}/logo`) .get(`/api/oidc/clients/${clientId}/logo`)
@@ -32,7 +37,7 @@ test('Edit OIDC client', async ({ page }) => {
await page.goto(`/settings/admin/oidc-clients/${oidcClient.id}`); await page.goto(`/settings/admin/oidc-clients/${oidcClient.id}`);
await page.getByLabel('Name').fill('Nextcloud updated'); await page.getByLabel('Name').fill('Nextcloud updated');
await page.getByLabel('Callback URL').fill('http://nextcloud-updated/auth/callback'); await page.getByTestId('callback-url-1').fill('http://nextcloud-updated/auth/callback');
await page.getByLabel('logo').setInputFiles('tests/assets/nextcloud-logo.png'); await page.getByLabel('logo').setInputFiles('tests/assets/nextcloud-logo.png');
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();