Feat: Cary over states using URL search params
This commit is contained in:
67
README.md
67
README.md
@ -45,9 +45,7 @@ Tested with the following Keycloak versions:
|
|||||||
- [Just changing the look](#just-changing-the-look)
|
- [Just changing the look](#just-changing-the-look)
|
||||||
- [Changing the look **and** feel](#changing-the-look-and-feel)
|
- [Changing the look **and** feel](#changing-the-look-and-feel)
|
||||||
- [Hot reload](#hot-reload)
|
- [Hot reload](#hot-reload)
|
||||||
- [How to implement context persistance](#how-to-implement-context-persistance)
|
- [Implement context persistance (optional)](#implement-context-persistance-optional)
|
||||||
- [If your keycloak is a subdomain of your app.](#if-your-keycloak-is-a-subdomain-of-your-app)
|
|
||||||
- [Else](#else)
|
|
||||||
- [GitHub Actions](#github-actions)
|
- [GitHub Actions](#github-actions)
|
||||||
- [REQUIREMENTS](#requirements)
|
- [REQUIREMENTS](#requirements)
|
||||||
- [API Reference](#api-reference)
|
- [API Reference](#api-reference)
|
||||||
@ -154,25 +152,64 @@ Checkout [this concrete example](https://github.com/garronej/keycloakify-demo-ap
|
|||||||
|
|
||||||
*NOTE: keycloak-react-theming was renamed keycloakify since this video was recorded*
|
*NOTE: keycloak-react-theming was renamed keycloakify since this video was recorded*
|
||||||
[](https://youtu.be/xTz0Rj7i2v8)
|
[](https://youtu.be/xTz0Rj7i2v8)
|
||||||
# How to implement context persistance
|
# Implement context persistance (optional)
|
||||||
|
|
||||||
If you want dark mode preference, language and others users preferences
|
If, before logging in, a user has selected a specific language
|
||||||
to persist within the page served by keycloak here are the methods you can
|
you don't want it to be reset to default when the user gets redirected to
|
||||||
adopt.
|
the login or register pages.
|
||||||
|
|
||||||
## If your keycloak is a subdomain of your app.
|
Same goes for the dark mode, you don't want, if the user had it enabled
|
||||||
|
to show the login page with light themes.
|
||||||
|
|
||||||
E.g: Your app url is `my-app.com` and your keycloak url is `auth.my-app.com`.
|
The problem is that you are probably using `localStorage` to persist theses values across
|
||||||
|
reload but, as the Keycloak pages are not served on the same domain that the rest of your
|
||||||
|
app you won't be able to carry over states using `localStorage`.
|
||||||
|
|
||||||
In this case there is a very straightforward approach and it is to use [`powerhooks/useGlobalState`](https://github.com/garronej/powerhooks).
|
The only reliable solution is to inject parameters into the URL before
|
||||||
Instead of `{ "persistance": "localStorage" }` use `{ "persistance": "cookie" }`.
|
redirecting to Keycloak. We integrate with
|
||||||
|
[`keycloak-js`](https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/javascript-adapter.adoc),
|
||||||
|
by providing you a way to tell `keycloak-js` that you would like to inject
|
||||||
|
some search parameters before redirecting.
|
||||||
|
|
||||||
## Else
|
The method also works with [`@react-keycloak/web`](https://www.npmjs.com/package/@react-keycloak/web) (use the `initOptions`).
|
||||||
|
|
||||||
You will have to use URL parameters to passes states when you redirect to
|
You can implement your own mechanism to pass the states in the URL and
|
||||||
the login page.
|
restore it on the other side but we recommend using `powerhooks/useGlobalState`
|
||||||
|
from the library [`powerhooks`](https://www.powerhooks.dev) that provide an elegant
|
||||||
|
way to handle states such as `isDarkModeEnabled` or `selectedLanguage`.
|
||||||
|
|
||||||
TOTO: Provide a clean way, as abstracted as possible, way to do that.
|
Let's modify [the example](https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/javascript-adapter.adoc) from the official `keycloak-js` documentation to
|
||||||
|
enables the states of `useGlobalStates` to be injected in the URL before redirecting.
|
||||||
|
Note that the states are automatically restored on the other side by `powerhooks`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import keycloak_js from "keycloak-js";
|
||||||
|
import { injectGlobalStatesInSearchParams } from "powerhooks/useGlobalState";
|
||||||
|
import { createKeycloakAdapter } from "keycloakify";
|
||||||
|
|
||||||
|
//...
|
||||||
|
|
||||||
|
const keycloakInstance = keycloak_js({
|
||||||
|
"url": "http://keycloak-server/auth",
|
||||||
|
"realm": "myrealm",
|
||||||
|
"clientId": "myapp"
|
||||||
|
});
|
||||||
|
|
||||||
|
keycloakInstance.init({
|
||||||
|
"onLoad": 'check-sso',
|
||||||
|
"silentCheckSsoRedirectUri": window.location.origin + "/silent-check-sso.html",
|
||||||
|
"adapter": createKeycloakAdapter({
|
||||||
|
"transformUrlBeforeRedirect": injectGlobalStatesInSearchParams,
|
||||||
|
keycloakInstance
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
//...
|
||||||
|
```
|
||||||
|
|
||||||
|
If you really want to go the extra miles and avoid having the white
|
||||||
|
flash of the blank html before the js bundle have been evaluated
|
||||||
|
[here is a snippet](https://github.com/InseeFrLab/onyxia-ui/blob/a77eb502870cfe6878edd0d956c646d28746d053/public/index.html#L5-L54) that you can place in your `public/index.html` if you are using `powerhooks/useGlobalState`.
|
||||||
|
|
||||||
# GitHub Actions
|
# GitHub Actions
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ export * from "./components/Info";
|
|||||||
export * from "./components/Error";
|
export * from "./components/Error";
|
||||||
export * from "./components/LoginResetPassword";
|
export * from "./components/LoginResetPassword";
|
||||||
export * from "./components/LoginVerifyEmail";
|
export * from "./components/LoginVerifyEmail";
|
||||||
|
export * from "./keycloakJsAdapter";
|
||||||
|
|
||||||
export * from "./tools/assert";
|
export * from "./tools/assert";
|
||||||
|
|
||||||
|
119
src/lib/keycloakJsAdapter.ts
Normal file
119
src/lib/keycloakJsAdapter.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export declare namespace keycloak_js {
|
||||||
|
|
||||||
|
export type KeycloakPromiseCallback<T> = (result: T) => void;
|
||||||
|
export class KeycloakPromise<TSuccess, TError> extends Promise<TSuccess> {
|
||||||
|
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>;
|
||||||
|
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>;
|
||||||
|
}
|
||||||
|
export interface KeycloakAdapter {
|
||||||
|
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||||
|
logout(options?: KeycloakLogoutOptions): KeycloakPromise<void, void>;
|
||||||
|
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||||
|
accountManagement(): KeycloakPromise<void, void>;
|
||||||
|
redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string;
|
||||||
|
}
|
||||||
|
export interface KeycloakLogoutOptions {
|
||||||
|
redirectUri?: string;
|
||||||
|
}
|
||||||
|
export interface KeycloakLoginOptions {
|
||||||
|
scope?: string;
|
||||||
|
redirectUri?: string;
|
||||||
|
prompt?: 'none' | 'login';
|
||||||
|
action?: string;
|
||||||
|
maxAge?: number;
|
||||||
|
loginHint?: string;
|
||||||
|
idpHint?: string;
|
||||||
|
locale?: string;
|
||||||
|
cordovaOptions?: { [optionName: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KeycloakInstance = Record<
|
||||||
|
"createLoginUrl" |
|
||||||
|
"createLogoutUrl" |
|
||||||
|
"createRegisterUrl",
|
||||||
|
(options: KeycloakLoginOptions | undefined) => string
|
||||||
|
> & {
|
||||||
|
createAccountUrl(): string;
|
||||||
|
redirectUri?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: This is just a slightly modified version of the default adapter in keycloak-js
|
||||||
|
* The goal here is just to be able to inject search param in url before keycloak redirect.
|
||||||
|
* Our use case for it is to pass over the login screen the states of useGlobalState
|
||||||
|
* namely isDarkModeEnabled, lgn...
|
||||||
|
*/
|
||||||
|
export function createKeycloakAdapter(
|
||||||
|
params: {
|
||||||
|
keycloakInstance: keycloak_js.KeycloakInstance;
|
||||||
|
transformUrlBeforeRedirect(url: string): string;
|
||||||
|
}
|
||||||
|
): keycloak_js.KeycloakAdapter {
|
||||||
|
|
||||||
|
const { keycloakInstance, transformUrlBeforeRedirect } = params;
|
||||||
|
|
||||||
|
const neverResolvingPromise: keycloak_js.KeycloakPromise<void, void> = Object.defineProperties(
|
||||||
|
new Promise(() => { }),
|
||||||
|
{
|
||||||
|
"success": { "value": () => { } },
|
||||||
|
"error": { "value": () => { } }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
"login": options => {
|
||||||
|
window.location.replace(
|
||||||
|
transformUrlBeforeRedirect(
|
||||||
|
keycloakInstance.createLoginUrl(
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return neverResolvingPromise;
|
||||||
|
},
|
||||||
|
"logout": options => {
|
||||||
|
window.location.replace(
|
||||||
|
transformUrlBeforeRedirect(
|
||||||
|
keycloakInstance.createLogoutUrl(
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return neverResolvingPromise;
|
||||||
|
},
|
||||||
|
"register": options => {
|
||||||
|
window.location.replace(
|
||||||
|
transformUrlBeforeRedirect(
|
||||||
|
keycloakInstance.createRegisterUrl(
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return neverResolvingPromise;
|
||||||
|
},
|
||||||
|
"accountManagement": () => {
|
||||||
|
var accountUrl = transformUrlBeforeRedirect(keycloakInstance.createAccountUrl());
|
||||||
|
if (typeof accountUrl !== 'undefined') {
|
||||||
|
window.location.href = accountUrl;
|
||||||
|
} else {
|
||||||
|
throw new Error("Not supported by the OIDC server");
|
||||||
|
}
|
||||||
|
return neverResolvingPromise;
|
||||||
|
},
|
||||||
|
"redirectUri": options => {
|
||||||
|
if (options && options.redirectUri) {
|
||||||
|
return options.redirectUri;
|
||||||
|
} else if (keycloakInstance.redirectUri) {
|
||||||
|
return keycloakInstance.redirectUri;
|
||||||
|
} else {
|
||||||
|
return window.location.href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user