updated OAuth2 providers ui

This commit is contained in:
Gani Georgiev
2023-04-13 15:38:12 +03:00
parent a77b62e5bd
commit af5f808144
74 changed files with 845 additions and 609 deletions
@@ -17,7 +17,7 @@
code: 200,
body: JSON.stringify(
{
token: "JWT_TOKEN",
token: "JWT_AUTH_TOKEN",
record: CommonHelper.dummyCollectionRecord(collection),
meta: {
id: "abc123",
@@ -25,6 +25,9 @@
username: "john.doe",
email: "test@example.com",
avatarUrl: "https://example.com/avatar.png",
accessToken: "...",
refreshToken: "...",
rawUser: {},
},
},
null,
@@ -52,11 +55,10 @@
<h3 class="m-b-sm">Auth with OAuth2 ({collection.name})</h3>
<div class="content txt-lg m-b-sm">
<p>Authenticate with an OAuth2 provider and returns a new auth token and record data.</p>
<p>This action usually should be called right after the provider login page redirect.</p>
<p>
You could also check the
For more details please check the
<a href={import.meta.env.PB_OAUTH2_EXAMPLE} target="_blank" rel="noopener noreferrer">
OAuth2 web integration example
OAuth2 integration documentation
</a>.
</p>
</div>
@@ -69,49 +71,46 @@
...
const authData = await pb.collection('${collection?.name}').authWithOAuth2(
'google',
'CODE',
'VERIFIER',
'REDIRECT_URL',
// optional data that will be used for the new account on OAuth2 sign-up
{
'name': 'test',
},
);
// This method initializes a one-off realtime subscription and will
// open a popup window with the OAuth2 vendor page to authenticate.
//
// Once the external OAuth2 sign-in/sign-up flow is completed, the popup
// window will be automatically closed and the OAuth2 data sent back
// to the user through the previously established realtime connection.
const authData = await pb.collection('users').authWithOAuth2({ provider: 'google' });
// after the above you can also access the auth data from the authStore
console.log(pb.authStore.isValid);
console.log(pb.authStore.token);
console.log(pb.authStore.model.id);
// "logout" the last authenticated account
// "logout" the last authenticated model
pb.authStore.clear();
`}
dart={`
import 'package:pocketbase/pocketbase.dart';
import 'package:url_launcher/url_launcher.dart';
final pb = PocketBase('${backendAbsUrl}');
...
final authData = await pb.collection('${collection?.name}').authWithOAuth2(
'google',
'CODE',
'VERIFIER',
'REDIRECT_URL',
// optional data that will be used for the new account on OAuth2 sign-up
createData: {
'name': 'test',
},
);
// This method initializes a one-off realtime subscription and will
// call the provided urlCallback with the OAuth2 vendor url to authenticate.
//
// Once the external OAuth2 sign-in/sign-up flow is completed, the browser
// window will be automatically closed and the OAuth2 data sent back
// to the user through the previously established realtime connection.
final authData = await pb.collection('users').authWithOAuth2('google', (url) async {
await launchUrl(url);
});
// after the above you can also access the auth data from the authStore
print(pb.authStore.isValid);
print(pb.authStore.token);
print(pb.authStore.model.id);
// "logout" the last authenticated account
// "logout" the last authenticated model
pb.authStore.clear();
`}
/>
@@ -1,114 +0,0 @@
<script>
import { scale, slide } from "svelte/transition";
import tooltip from "@/actions/tooltip";
import { errors, removeError } from "@/stores/errors";
import CommonHelper from "@/utils/CommonHelper";
import Accordion from "@/components/base/Accordion.svelte";
import Field from "@/components/base/Field.svelte";
import RedactedPasswordInput from "@/components/base/RedactedPasswordInput.svelte";
export let key;
export let title;
export let icon = "";
export let config = {};
export let optionsComponent;
let accordion;
$: hasErrors = !CommonHelper.isEmpty(CommonHelper.getNestedVal($errors, key));
$: if (!config.enabled) {
removeError(key);
}
export function expand() {
accordion?.expand();
}
export function collapse() {
accordion?.collapse();
}
export function collapseSiblings() {
accordion?.collapseSiblings();
}
function clear() {
for (let k in config) {
config[k] = "";
}
config.enabled = false;
}
</script>
<Accordion bind:this={accordion} on:expand on:collapse on:toggle {...$$restProps}>
<svelte:fragment slot="header">
<div class="inline-flex">
{#if icon}
<i class={icon} class:txt-hint={!config.enabled} />
{/if}
<span class="txt">{title}</span>
<em class="txt-hint">
({key.substring(0, key.length - 4)})
</em>
</div>
<div class="flex-fill" />
{#if hasErrors}
<i
class="ri-error-warning-fill txt-danger"
transition:scale|local={{ duration: 150, start: 0.7 }}
use:tooltip={{ text: "Has errors", position: "left" }}
/>
{/if}
{#if config.enabled}
<span class="label label-success">Enabled</span>
{:else}
<span class="label label-hint">Disabled</span>
{/if}
</svelte:fragment>
<div class="flex">
<Field class="form-field form-field-toggle m-b-0" name="{key}.enabled" let:uniqueId>
<input type="checkbox" id={uniqueId} bind:checked={config.enabled} />
<label for={uniqueId}>Enable</label>
</Field>
<button type="button" class="btn btn-sm btn-transparent btn-hint m-l-auto" on:click={clear}>
<span class="txt">Clear all fields</span>
</button>
</div>
<div class="grid">
<div class="col-12 spacing" />
<div class="col-lg-6">
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.clientId" let:uniqueId>
<label for={uniqueId}>Client ID</label>
<input type="text" id={uniqueId} bind:value={config.clientId} required={config.enabled} />
</Field>
</div>
<div class="col-lg-6">
<Field
class="form-field {config.enabled ? 'required' : ''}"
name="{key}.clientSecret"
let:uniqueId
>
<label for={uniqueId}>Client secret</label>
<RedactedPasswordInput
bind:value={config.clientSecret}
id={uniqueId}
required={config.enabled}
/>
</Field>
</div>
{#if optionsComponent}
<div class="col-lg-12">
<svelte:component this={optionsComponent} {key} bind:config />
</div>
{/if}
</div>
</Accordion>
@@ -0,0 +1,78 @@
<script>
import AuthProviderPanel from "@/components/settings/AuthProviderPanel.svelte";
export let provider = {};
export let config = {};
let providerPanel;
</script>
<div class="provider-card">
<figure class="provider-logo">
{#if provider.logo}
<img src={provider.logo} alt="{provider.title} logo" />
{/if}
</figure>
<div class="title">{provider.title}</div>
<em class="txt-hint txt-sm m-r-auto">({provider.key.slice(0, -4)})</em>
{#if config.enabled}
<div class="label label-success">Enabled</div>
{/if}
<button
type="button"
class="btn btn-circle btn-hint btn-transparent"
aria-label="Provider settings"
on:click={() => {
providerPanel?.show(
provider,
Object.assign({}, config, {
enabled: config.clientId ? config.enabled : true,
})
);
}}
>
<i class="ri-settings-4-line" />
</button>
</div>
<AuthProviderPanel
bind:this={providerPanel}
on:submit={(e) => {
if (e.detail[provider.key]) {
config = e.detail[provider.key];
}
}}
/>
<style lang="scss">
.provider-logo {
$boxSize: 32px;
$imgSize: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: $boxSize;
height: $boxSize;
border-radius: var(--baseRadius);
background: var(--bodyColor);
padding: 0;
gap: 0;
img {
max-width: $imgSize;
max-height: $imgSize;
height: auto;
flex-shrink: 0;
}
}
.provider-card {
display: flex;
align-items: center;
width: 100%;
height: 100%;
gap: 10px;
padding: 10px;
border-radius: var(--baseRadius);
border: 1px solid var(--baseAlt1Color);
}
</style>
@@ -0,0 +1,124 @@
<script>
import { createEventDispatcher } from "svelte";
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import { setErrors } from "@/stores/errors";
import { addSuccessToast } from "@/stores/toasts";
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
import Field from "@/components/base/Field.svelte";
import RedactedPasswordInput from "@/components/base/RedactedPasswordInput.svelte";
const dispatch = createEventDispatcher();
const formId = "provider_popup_" + CommonHelper.randomString(5);
let panel;
let provider = {};
let config = {};
let isSubmitting = false;
let initialHash = "";
$: hasChanges = JSON.stringify(config) != initialHash;
export function show(showProvider, showConfig) {
setErrors({}); // reset any previous errors
provider = Object.assign({}, showProvider);
config = Object.assign({ enabled: true }, showConfig);
initialHash = JSON.stringify(config);
panel?.show();
}
export function hide() {
return panel?.hide();
}
async function submit() {
isSubmitting = true;
try {
const data = {};
data[provider.key] = CommonHelper.filterRedactedProps(config);
const result = await ApiClient.settings.update(data);
setErrors({});
addSuccessToast("Successfully updated provider settings.");
dispatch("submit", result);
hide();
} catch (err) {
ApiClient.errorResponseHandler(err);
}
isSubmitting = false;
}
function clear() {
for (let k in config) {
config[k] = "";
}
config.enabled = false;
}
</script>
<OverlayPanel bind:this={panel} overlayClose={!isSubmitting} escClose={!isSubmitting} on:show on:hide>
<svelte:fragment slot="header">
<h4 class="center txt-break">{provider.title || provider.key} provider</h4>
</svelte:fragment>
<form id={formId} autocomplete="off" on:submit|preventDefault={() => submit()}>
<div class="flex m-b-base">
<Field class="form-field form-field-toggle m-b-0" name="{provider.key}.enabled" let:uniqueId>
<input type="checkbox" id={uniqueId} bind:checked={config.enabled} />
<label for={uniqueId}>Enable</label>
</Field>
<button type="button" class="btn btn-sm btn-transparent btn-hint m-l-auto" on:click={clear}>
<span class="txt">Clear all fields</span>
</button>
</div>
<Field
class="form-field {config.enabled ? 'required' : ''}"
name="{provider.key}.clientId"
let:uniqueId
>
<label for={uniqueId}>Client ID</label>
<input type="text" id={uniqueId} bind:value={config.clientId} required={config.enabled} />
</Field>
<Field
class="form-field {config.enabled ? 'required' : ''}"
name="{provider.key}.clientSecret"
let:uniqueId
>
<label for={uniqueId}>Client secret</label>
<RedactedPasswordInput bind:value={config.clientSecret} id={uniqueId} required={config.enabled} />
</Field>
{#if provider.optionsComponent}
<div class="col-lg-12">
<svelte:component this={provider.optionsComponent} key={provider.key} bind:config />
</div>
{/if}
</form>
<svelte:fragment slot="footer">
<button type="button" class="btn btn-transparent" on:click={hide} disabled={isSubmitting}>
Close
</button>
<button
type="submit"
form={formId}
class="btn btn-expanded"
class:btn-loading={isSubmitting}
disabled={!hasChanges || isSubmitting}
>
<span class="txt">Save changes</span>
</button>
</svelte:fragment>
</OverlayPanel>
@@ -1,28 +1,19 @@
<script>
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import { pageTitle } from "@/stores/app";
import { setErrors } from "@/stores/errors";
import { addSuccessToast } from "@/stores/toasts";
import PageWrapper from "@/components/base/PageWrapper.svelte";
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
import AuthProviderAccordion from "@/components/settings/AuthProviderAccordion.svelte";
import AuthProviderCard from "@/components/settings/AuthProviderCard.svelte";
import providersList from "@/providers.js";
$pageTitle = "Auth providers";
let accordions = {};
let originalFormSettings = {};
let formSettings = {};
let isLoading = false;
let isSaving = false;
let showHidden = false;
let formSettings = {};
$: initialHash = JSON.stringify(originalFormSettings);
$: enabledProviders = providersList.filter((provider) => formSettings[provider.key]?.enabled);
$: hasChanges = initialHash != JSON.stringify(formSettings);
$: totalHidden = Object.values(providersList).filter((provider) => provider.hidden).length;
$: disabledProviders = providersList.filter((provider) => !formSettings[provider.key]?.enabled);
loadSettings();
@@ -39,41 +30,13 @@
isLoading = false;
}
async function save() {
if (isSaving || !hasChanges) {
return;
}
isSaving = true;
try {
const result = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
initSettings(result);
setErrors({});
accordions[Object.keys(accordions)[0]]?.collapseSiblings();
addSuccessToast("Successfully updated auth providers.");
} catch (err) {
ApiClient.errorResponseHandler(err);
}
isSaving = false;
}
function initSettings(data) {
data = data || {};
formSettings = {};
for (const providerKey in providersList) {
formSettings[providerKey] = Object.assign({ enabled: false }, data[providerKey]);
for (const provider of providersList) {
formSettings[provider.key] = Object.assign({ enabled: false }, data[provider.key]);
}
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
}
function reset() {
formSettings = JSON.parse(JSON.stringify(originalFormSettings || {}));
}
</script>
@@ -88,61 +51,32 @@
</header>
<div class="wrapper">
<form class="panel" autocomplete="off" on:submit|preventDefault={save}>
<div class="panel">
<h6 class="m-b-base">Manage the allowed users OAuth2 sign-in/sign-up methods.</h6>
{#if isLoading}
<div class="loader" />
{:else}
<div class="accordions">
{#each Object.entries(providersList) as [key, provider]}
{#if showHidden || !provider.hidden || formSettings[key]?.enabled}
<AuthProviderAccordion
bind:this={accordions[key]}
single
{key}
title={provider.title}
icon={provider.icon || "ri-fingerprint-line"}
optionsComponent={provider.optionsComponent}
bind:config={formSettings[key]}
/>
{/if}
<div class="grid grid-sm">
{#each enabledProviders as provider (provider.key)}
<div class="col-lg-6">
<AuthProviderCard {provider} bind:config={formSettings[provider.key]} />
</div>
{/each}
</div>
{#if !showHidden}
<button
type="button"
class="btn btn-sm btn-transparent btn-hint m-t-10"
on:click={() => (showHidden = true)}
>
<i class="ri-arrow-down-s-line" />
<span class="txt">Show all ({totalHidden})</span>
</button>
{#if enabledProviders.length > 0}
<hr />
{/if}
<div class="flex m-t-base">
<div class="flex-fill" />
{#if hasChanges}
<button
type="button"
class="btn btn-transparent btn-hint"
disabled={isSaving}
on:click={() => reset()}
>
<span class="txt">Cancel</span>
</button>
{/if}
<button
type="submit"
class="btn btn-expanded"
class:btn-loading={isSaving}
disabled={!hasChanges || isSaving}
>
<span class="txt">Save changes</span>
</button>
<div class="grid grid-sm">
{#each disabledProviders as provider (provider.key)}
<div class="col-lg-6">
<AuthProviderCard {provider} bind:config={formSettings[provider.key]} />
</div>
{/each}
</div>
{/if}
</form>
</div>
</div>
</PageWrapper>
@@ -6,23 +6,17 @@
</script>
<div class="section-title">Azure AD endpoints</div>
<div class="grid">
<div class="col-lg-12">
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.authUrl" let:uniqueId>
<label for={uniqueId}>Auth URL</label>
<input type="url" id={uniqueId} bind:value={config.authUrl} required={config.enabled} />
<div class="help-block">
Eg. {`https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/authorize`}
</div>
</Field>
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.authUrl" let:uniqueId>
<label for={uniqueId}>Auth URL</label>
<input type="url" id={uniqueId} bind:value={config.authUrl} required={config.enabled} />
<div class="help-block">
Eg. {`https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/authorize`}
</div>
<div class="col-lg-12">
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.tokenUrl" let:uniqueId>
<label for={uniqueId}>Token URL</label>
<input type="url" id={uniqueId} bind:value={config.tokenUrl} required={config.enabled} />
<div class="help-block">
Eg. {`https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/token`}
</div>
</Field>
</Field>
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.tokenUrl" let:uniqueId>
<label for={uniqueId}>Token URL</label>
<input type="url" id={uniqueId} bind:value={config.tokenUrl} required={config.enabled} />
<div class="help-block">
Eg. {`https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/token`}
</div>
</div>
</Field>
@@ -6,26 +6,18 @@
</script>
<div class="section-title">Endpoints</div>
<div class="grid">
<div class="col-lg-12">
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.authUrl" let:uniqueId>
<label for={uniqueId}>Auth URL</label>
<input type="url" id={uniqueId} bind:value={config.authUrl} required={config.enabled} />
<div class="help-block">Eg. https://example.com/authorize/</div>
</Field>
</div>
<div class="col-lg-12">
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.tokenUrl" let:uniqueId>
<label for={uniqueId}>Token URL</label>
<input type="url" id={uniqueId} bind:value={config.tokenUrl} required={config.enabled} />
<div class="help-block">Eg. https://example.com/token/</div>
</Field>
</div>
<div class="col-lg-12">
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.userApiUrl" let:uniqueId>
<label for={uniqueId}>User API URL</label>
<input type="url" id={uniqueId} bind:value={config.userApiUrl} required={config.enabled} />
<div class="help-block">Eg. https://example.com/userinfo/</div>
</Field>
</div>
</div>
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.authUrl" let:uniqueId>
<label for={uniqueId}>Auth URL</label>
<input type="url" id={uniqueId} bind:value={config.authUrl} required={config.enabled} />
<div class="help-block">Eg. https://example.com/authorize/</div>
</Field>
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.tokenUrl" let:uniqueId>
<label for={uniqueId}>Token URL</label>
<input type="url" id={uniqueId} bind:value={config.tokenUrl} required={config.enabled} />
<div class="help-block">Eg. https://example.com/token/</div>
</Field>
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.userApiUrl" let:uniqueId>
<label for={uniqueId}>User API URL</label>
<input type="url" id={uniqueId} bind:value={config.userApiUrl} required={config.enabled} />
<div class="help-block">Eg. https://example.com/userinfo/</div>
</Field>
@@ -6,23 +6,15 @@
</script>
<div class="section-title">Selfhosted endpoints (optional)</div>
<div class="grid">
<div class="col-lg-4">
<Field class="form-field" name="{key}.authUrl" let:uniqueId>
<label for={uniqueId}>Auth URL</label>
<input type="url" id={uniqueId} bind:value={config.authUrl} />
</Field>
</div>
<div class="col-lg-4">
<Field class="form-field" name="{key}.tokenUrl" let:uniqueId>
<label for={uniqueId}>Token URL</label>
<input type="url" id={uniqueId} bind:value={config.tokenUrl} />
</Field>
</div>
<div class="col-lg-4">
<Field class="form-field" name="{key}.userApiUrl" let:uniqueId>
<label for={uniqueId}>User API URL</label>
<input type="url" id={uniqueId} bind:value={config.userApiUrl} />
</Field>
</div>
</div>
<Field class="form-field" name="{key}.authUrl" let:uniqueId>
<label for={uniqueId}>Auth URL</label>
<input type="url" id={uniqueId} bind:value={config.authUrl} />
</Field>
<Field class="form-field" name="{key}.tokenUrl" let:uniqueId>
<label for={uniqueId}>Token URL</label>
<input type="url" id={uniqueId} bind:value={config.tokenUrl} />
</Field>
<Field class="form-field" name="{key}.userApiUrl" let:uniqueId>
<label for={uniqueId}>User API URL</label>
<input type="url" id={uniqueId} bind:value={config.userApiUrl} />
</Field>
+64 -48
View File
@@ -5,92 +5,108 @@ import AppleOptions from "@/components/settings/providers/AppleOptions.svel
// Object list with all supported OAuth2 providers in the format:
// ```
// { settingsKey: { title, icon, hidden, optionsComponent? } }
// [ { key, title, logo, optionsComponent? }, ... ]
// ```
//
// If `optionsComponent` is provided it will receive 2 parameters:
// - `key` - the provider settings key (eg. "gitlabAuth")
// - `config` - the provider settings config that is currently being updated
export default {
appleAuth: {
export default [
{
key: "appleAuth",
title: "Apple",
icon: "ri-apple-fill",
logo: "/images/oauth2/apple.svg",
optionsComponent: AppleOptions,
},
googleAuth: {
{
key: "googleAuth",
title: "Google",
icon: "ri-google-fill",
logo: "/images/oauth2/google.svg",
},
microsoftAuth: {
{
key: "facebookAuth",
title: "Facebook",
logo: "/images/oauth2/facebook.svg",
},
{
key: "microsoftAuth",
title: "Microsoft",
icon: "ri-microsoft-fill",
logo: "/images/oauth2/microsoft.svg",
optionsComponent: MicrosoftOptions,
},
facebookAuth: {
title: "Facebook",
icon: "ri-facebook-fill",
},
twitterAuth: {
title: "Twitter",
icon: "ri-twitter-fill",
},
githubAuth: {
{
key: "githubAuth",
title: "GitHub",
icon: "ri-github-fill",
logo: "/images/oauth2/github.svg",
},
gitlabAuth: {
{
key: "gitlabAuth",
title: "GitLab",
icon: "ri-gitlab-fill",
logo: "/images/oauth2/gitlab.svg",
optionsComponent: SelfHostedOptions,
},
giteeAuth: {
{
key: "giteeAuth",
title: "Gitee",
icon: "ri-git-repository-fill",
logo: "/images/oauth2/gitee.svg",
},
giteaAuth: {
{
key: "giteaAuth",
title: "Gitea",
icon: "ri-cup-fill",
logo: "/images/oauth2/gitea.svg",
optionsComponent: SelfHostedOptions,
},
discordAuth: {
{
key: "discordAuth",
title: "Discord",
icon: "ri-discord-fill",
logo: "/images/oauth2/discord.svg",
},
kakaoAuth: {
{
key: "twitterAuth",
title: "Twitter",
logo: "/images/oauth2/twitter.svg",
},
{
key: "kakaoAuth",
title: "Kakao",
icon: "ri-kakao-talk-fill",
logo: "/images/oauth2/kakao.svg",
},
spotifyAuth: {
{
key: "spotifyAuth",
title: "Spotify",
icon: "ri-spotify-fill",
logo: "/images/oauth2/spotify.svg",
},
twitchAuth: {
{
key: "twitchAuth",
title: "Twitch",
icon: "ri-twitch-fill",
logo: "/images/oauth2/twitch.svg",
},
stravaAuth: {
{
key: "stravaAuth",
title: "Strava",
icon: "ri-riding-fill",
logo: "/images/oauth2/strava.svg",
},
livechatAuth: {
{
key: "livechatAuth",
title: "LiveChat",
icon: "ri-chat-1-fill",
logo: "/images/oauth2/livechat.svg",
},
oidcAuth: {
title: "OpenID Connect - Authentik, Keycloak, Okta, etc.",
icon: "ri-lock-fill",
{
key: "oidcAuth",
title: "OpenID Connect",
logo: "/images/oauth2/oidc.svg",
optionsComponent: OIDCOptions,
},
oidc2Auth: {
title: "(2) OpenID Connect - Authentik, Keycloak, Okta, etc.",
icon: "ri-lock-fill",
hidden: true,
{
key: "oidc2Auth",
title: "(2) OpenID Connect",
logo: "/images/oauth2/oidc.svg",
optionsComponent: OIDCOptions,
},
oidc3Auth: {
title: "(3) OpenID Connect - Authentik, Keycloak, Okta, etc.",
icon: "ri-lock-fill",
hidden: true,
{
key: "oidc3Auth",
title: "(3) OpenID Connect",
logo: "/images/oauth2/oidc.svg",
optionsComponent: OIDCOptions,
},
};
];