This commit is contained in:
technofab 2025-01-02 20:26:59 +01:00
parent 7a1b1f6af0
commit 2cc87c0b95

View file

@ -1,199 +1,181 @@
<script context="module"> <script context="module">
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import oidcClient from 'oidc-client'; import oidcClient from 'oidc-client';
import { onMount, onDestroy, setContext } from 'svelte'; import { onMount, onDestroy, setContext } from 'svelte';
/** /**
* Stores * Stores
*/ */
export const isLoading = writable(true); export const isLoading = writable(true);
export const isAuthenticated = writable(false); export const isAuthenticated = writable(false);
export const accessToken = writable(''); export const accessToken = writable('');
export const idToken = writable(''); export const idToken = writable('');
export const userInfo = writable({}); export const userInfo = writable({});
export const authError = writable(null); export const authError = writable(null);
/** /**
* Context Keys * Context Keys
* *
* using an object literal means the keys are guaranteed not to conflict in any circumstance (since an object only has * using an object literal means the keys are guaranteed not to conflict in any circumstance (since an object only has
* referential equality to itself, i.e. {} !== {} whereas "x" === "x"), even when you have multiple different contexts * referential equality to itself, i.e. {} !== {} whereas "x" === "x"), even when you have multiple different contexts
* operating across many component layers. * operating across many component layers.
*/ */
export const OIDC_CONTEXT_CLIENT_PROMISE = {}; export const OIDC_CONTEXT_CLIENT_PROMISE = {};
export const OIDC_CONTEXT_REDIRECT_URI = {}; export const OIDC_CONTEXT_REDIRECT_URI = {};
export const OIDC_CONTEXT_POST_LOGOUT_REDIRECT_URI = {}; export const OIDC_CONTEXT_POST_LOGOUT_REDIRECT_URI = {};
/** /**
* Refresh the accessToken using the silentRenew method (hidden iframe) * Refresh the accessToken using the silentRenew method (hidden iframe)
* *
* @param {Promise<UserManager>} oidcPromise * @param {Promise<UserManager>} oidcPromise
* @return bool indicated whether the token was refreshed, if false error will be set * @return bool indicated whether the token was refreshed, if false error will be set
* in the authError store. * in the authError store.
*/ */
export async function refreshToken(oidcPromise) { export async function refreshToken(oidcPromise) {
try { try {
const oidc = await oidcPromise const oidc = await oidcPromise
await oidc.signinSilent(); await oidc.signinSilent();
return true; return true;
} }
catch (e) { catch (e) {
// set error state for reactive handling // set error state for reactive handling
authError.set(e.message); authError.set(e.message);
return false; return false;
} }
} }
/** /**
* Initiate Register/Login flow. * Initiate Register/Login flow.
* *
* @param {Promise<UserManager>} oidcPromise * @param {Promise<UserManager>} oidcPromise
* @param {boolean} preserveRoute - store current location so callback handler will navigate back to it. * @param {boolean} preserveRoute - store current location so callback handler will navigate back to it.
* @param {string} callback_url - explicit path to use for the callback. * @param {string} callback_url - explicit path to use for the callback.
*/ */
export async function login(oidcPromise, preserveRoute = true, callback_url = null) { export async function login(oidcPromise, preserveRoute = true, callback_url = null) {
const oidc = await oidcPromise; const oidc = await oidcPromise;
const redirect_uri = callback_url || window.location.href; const redirect_uri = callback_url || window.location.href;
// try to keep the user on the same page from which they triggered login. If set to false should typically // try to keep the user on the same page from which they triggered login. If set to false should typically
// cause redirect to /. // cause redirect to /.
const appState = preserveRoute const appState = preserveRoute
? { ? {
pathname: window.location.pathname, pathname: window.location.pathname,
search: window.location.search, search: window.location.search,
} }
: {}; : {};
await oidc.signinRedirect({ redirect_uri, appState }); await oidc.signinRedirect({ redirect_uri, appState });
} }
/** /**
* Log out the current user. * Log out the current user.
* *
* @param {Promise<UserManager>} oidcPromise * @param {Promise<UserManager>} oidcPromise
* @param {string} logout_url - specify the url to return to after login. * @param {string} logout_url - specify the url to return to after login.
*/ */
export async function logout(oidcPromise, logout_url = null) { export async function logout(oidcPromise, logout_url = null) {
const oidc = await oidcPromise; const oidc = await oidcPromise;
const returnTo = logout_url || window.location.href; const returnTo = logout_url || window.location.href;
oidc.signoutRedirect({ returnTo }); oidc.signoutRedirect({ returnTo });
try { const response = await oidc.signoutRedirect({ returnTo });
const response = await oidc.signoutRedirect({ returnTo }); }
} catch (err) {
if (err.message !== 'no end session endpoint') throw err;
// this is most likely auth0, so let's try their logout endpoint.
// @see: https://auth0.com/docs/api/authentication#logout
// this is dirty and hack and reaches into guts of the oidc client
// in ways I'd prefer not to.. but auth0 has this annoying non-conforming
// session termination.
const authority = oidc._settings._authority;
if (authority.endsWith('auth0.com')) {
const clientId = oidc._settings._client_id;
const url = `${authority}/v2/logout?client_id=${clientId}&returnTo=${encodeURIComponent(
returnTo
)}`;
window.location = url;
} else throw err
}
}
</script> </script>
<script> <script>
// props. // props.
export let issuer; export let issuer;
export let client_id; export let client_id;
export let redirect_uri; export let redirect_uri;
export let post_logout_redirect_uri; export let post_logout_redirect_uri;
export let extraOptions = {}; export let extraOptions = {};
export let scope = 'openid profile email'; export let scope = 'openid profile email';
setContext(OIDC_CONTEXT_REDIRECT_URI, redirect_uri); setContext(OIDC_CONTEXT_REDIRECT_URI, redirect_uri);
setContext(OIDC_CONTEXT_POST_LOGOUT_REDIRECT_URI, post_logout_redirect_uri); setContext(OIDC_CONTEXT_POST_LOGOUT_REDIRECT_URI, post_logout_redirect_uri);
const settings = { const settings = {
authority: issuer, authority: issuer,
client_id, client_id,
redirect_uri, redirect_uri,
post_logout_redirect_uri, post_logout_redirect_uri,
response_type: 'code', response_type: 'code',
scope, scope,
automaticSilentRenew: true, automaticSilentRenew: true,
...extraOptions, ...extraOptions,
}; };
const userManager = new oidcClient.UserManager(settings); const userManager = new oidcClient.UserManager(settings);
userManager.events.addUserLoaded(function (user) { userManager.events.addUserLoaded(function (user) {
isAuthenticated.set(true); isAuthenticated.set(true);
accessToken.set(user.access_token); accessToken.set(user.access_token);
idToken.set(user.id_token); idToken.set(user.id_token);
userInfo.set(user.profile); userInfo.set(user.profile);
}); isLoading.set(false);
});
userManager.events.addUserUnloaded(function() { userManager.events.addUserUnloaded(function() {
isAuthenticated.set(false); isAuthenticated.set(false);
idToken.set(''); idToken.set('');
accessToken.set(''); accessToken.set('');
userInfo.set({}); userInfo.set({});
}); });
userManager.events.addSilentRenewError(function(e) { userManager.events.addSilentRenewError(function(e) {
authError.set(`SilentRenewError: ${e.message}`); authError.set(`SilentRenewError: ${e.message}`);
}); });
// does userManager needs to be wrapped in a promise? or is this a left over to maintain // does userManager needs to be wrapped in a promise? or is this a left over to maintain
// symmetry with the @dopry/svelte-auth0 auth0 implementation // symmetry with the @dopry/svelte-auth0 auth0 implementation
let oidcPromise = Promise.resolve(userManager); let oidcPromise = Promise.resolve(userManager);
setContext(OIDC_CONTEXT_CLIENT_PROMISE, oidcPromise); setContext(OIDC_CONTEXT_CLIENT_PROMISE, oidcPromise);
// Not all browsers support this, please program defensively! // Not all browsers support this, please program defensively!
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
// Use 'error' and 'code' to test if the component is being executed as a part of a login callback. If we're not // Use 'error' and 'code' to test if the component is being executed as a part of a login callback. If we're not
// running in a login callback, and the user isn't logged in, see if we can capture their existing session. // running in a login callback, and the user isn't logged in, see if we can capture their existing session.
if (!params.has('error') && !params.has('code') && !$isAuthenticated) { if (!params.has('error') && !params.has('code') && !$isAuthenticated) {
refreshToken(oidcPromise); refreshToken(oidcPromise);
}
async function handleOnMount() {
// on run onMount after oidc
const oidc = await oidcPromise;
// Check if something went wrong during login redirect
// and extract the error message
if (params.has('error')) {
authError.set(new Error(params.get('error_description')));
} }
async function handleOnMount() { // if code then login success
// on run onMount after oidc if (params.has('code')) {
const oidc = await oidcPromise; // handle the callback
const response = await oidc.signinCallback();
let state = (response && response.state) || {};
// Can be smart here and redirect to original path instead of root
const url = state && state.targetUrl ? state.targetUrl : window.location.pathname;
state = { ...state, isRedirectCallback: true };
// Check if something went wrong during login redirect // redirect to the last page we were on when login was configured if it was passed.
// and extract the error message history.replaceState(state, '', url);
if (params.has('error')) { // location.href = url;
authError.set(new Error(params.get('error_description'))); // clear errors on login.
} authError.set(null);
}
// if code was not set and there was a state, then we're in an auth callback and there was an error. We still
// need to wrap the sign-in silent. We need to sit down and chart out the various success and fail scenarios and
// what the uris loook like. I fear this may be problematic in other auth flows in the future.
else if (params.has('state')) {
const response = await oidc.signinCallback();
}
}
async function handleOnDestroy() {}
// if code then login success onMount(handleOnMount);
if (params.has('code')) { onDestroy(handleOnDestroy);
// handle the callback
const response = await oidc.signinCallback();
let state = (response && response.state) || {};
// Can be smart here and redirect to original path instead of root
const url = state && state.targetUrl ? state.targetUrl : window.location.pathname;
state = { ...state, isRedirectCallback: true };
// redirect to the last page we were on when login was configured if it was passed.
history.replaceState(state, '', url);
// location.href = url;
// clear errors on login.
authError.set(null);
}
// if code was not set and there was a state, then we're in an auth callback and there was an error. We still
// need to wrap the sign-in silent. We need to sit down and chart out the various success and fail scenarios and
// what the uris loook like. I fear this may be problematic in other auth flows in the future.
else if (params.has('state')) {
const response = await oidc.signinCallback();
console.log('oidc.signinCallback::response', response)
}
isLoading.set(false);
}
async function handleOnDestroy() {}
onMount(handleOnMount);
onDestroy(handleOnDestroy);
</script> </script>
<slot /> <slot />