Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
0c0052e1cd | |||
78622770ec | |||
7b86727394 | |||
0965f8648e | |||
98974b4367 | |||
597bcadd9e | |||
4d9aabcb91 | |||
1606c2884d | |||
12f69b593f | |||
1ca45f90d0 | |||
a91a5616f9 | |||
c525e09368 | |||
f5bba4a6a0 | |||
77a37fb573 | |||
6b24c5878c |
18
CHANGELOG.md
18
CHANGELOG.md
@ -1,3 +1,21 @@
|
|||||||
|
### **0.3.3** (2021-03-22)
|
||||||
|
|
||||||
|
- Fix submit not receving correct text
|
||||||
|
|
||||||
|
### **0.3.2** (2021-03-21)
|
||||||
|
|
||||||
|
- Fix broken previous release
|
||||||
|
|
||||||
|
### **0.3.1** (2021-03-21)
|
||||||
|
|
||||||
|
- kcHeaderClass can be updated after initial mount
|
||||||
|
|
||||||
|
## **0.3.0** (2021-03-20)
|
||||||
|
|
||||||
|
- Bump version
|
||||||
|
- Feat: Cary over states using URL search params
|
||||||
|
- Bugfix: with kcHtmlClass
|
||||||
|
|
||||||
### **0.2.10** (2021-03-19)
|
### **0.2.10** (2021-03-19)
|
||||||
|
|
||||||
- Remove dependency to denoify
|
- Remove dependency to denoify
|
||||||
|
82
README.md
82
README.md
@ -45,14 +45,11 @@ 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)
|
|
||||||
- [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)
|
||||||
- [The build tool](#the-build-tool)
|
- [The build tool](#the-build-tool)
|
||||||
- [The fronted lib ( imported into your react app )](#the-fronted-lib--imported-into-your-react-app-)
|
- [Implement context persistance (optional)](#implement-context-persistance-optional)
|
||||||
|
|
||||||
# How to use
|
# How to use
|
||||||
## Setting up the build tool
|
## Setting up the build tool
|
||||||
@ -154,25 +151,6 @@ 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
|
|
||||||
|
|
||||||
If you want dark mode preference, language and others users preferences
|
|
||||||
to persist within the page served by keycloak here are the methods you can
|
|
||||||
adopt.
|
|
||||||
|
|
||||||
## If your keycloak is a subdomain of your app.
|
|
||||||
|
|
||||||
E.g: Your app url is `my-app.com` and your keycloak url is `auth.my-app.com`.
|
|
||||||
|
|
||||||
In this case there is a very straightforward approach and it is to use [`powerhooks/useGlobalState`](https://github.com/garronej/powerhooks).
|
|
||||||
Instead of `{ "persistance": "localStorage" }` use `{ "persistance": "cookie" }`.
|
|
||||||
|
|
||||||
## Else
|
|
||||||
|
|
||||||
You will have to use URL parameters to passes states when you redirect to
|
|
||||||
the login page.
|
|
||||||
|
|
||||||
TOTO: Provide a clean way, as abstracted as possible, way to do that.
|
|
||||||
|
|
||||||
# GitHub Actions
|
# GitHub Actions
|
||||||
|
|
||||||
@ -204,9 +182,61 @@ Part of the lib that runs with node, at build time.
|
|||||||
- `npx build-keycloak-theme`: Builds the theme, the CWD is assumed to be the root of your react project.
|
- `npx build-keycloak-theme`: Builds the theme, the CWD is assumed to be the root of your react project.
|
||||||
- `npx download-sample-keycloak-themes`: Downloads the keycloak default themes (for development purposes)
|
- `npx download-sample-keycloak-themes`: Downloads the keycloak default themes (for development purposes)
|
||||||
|
|
||||||
## The fronted lib ( imported into your react app )
|
# Implement context persistance (optional)
|
||||||
|
|
||||||
Part of the lib that you import in your react project and runs on the browser.
|
If, before logging in, a user has selected a specific language
|
||||||
|
you don't want it to be reset to default when the user gets redirected to
|
||||||
|
the login or register pages.
|
||||||
|
|
||||||
**TODO**
|
Same goes for the dark mode, you don't want, if the user had it enabled
|
||||||
|
to show the login page with light themes.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
The only reliable solution is to inject parameters into the URL before
|
||||||
|
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.
|
||||||
|
|
||||||
|
The method also works with [`@react-keycloak/web`](https://www.npmjs.com/package/@react-keycloak/web) (use the `initOptions`).
|
||||||
|
|
||||||
|
You can implement your own mechanism to pass the states in the URL and
|
||||||
|
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`.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "0.2.10",
|
"version": "0.3.3",
|
||||||
"description": "Keycloak theme generator for Reacts app",
|
"description": "Keycloak theme generator for Reacts app",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -66,7 +66,7 @@ export const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: Kc
|
|||||||
props.kcButtonBlockClass, props.kcButtonLargeClass
|
props.kcButtonBlockClass, props.kcButtonLargeClass
|
||||||
)}
|
)}
|
||||||
type="submit"
|
type="submit"
|
||||||
defaultValue={msgStr("doSubmit")}
|
value={msgStr("doSubmit")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,7 +113,7 @@ export const Register = memo(({ kcContext, ...props }: { kcContext: KcContext.Re
|
|||||||
|
|
||||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
||||||
<input className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} type="submit"
|
<input className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} type="submit"
|
||||||
defaultValue={msgStr("doRegister")} />
|
value={msgStr("doRegister")} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form >
|
</form >
|
||||||
|
@ -27,7 +27,6 @@ export type TemplateProps = {
|
|||||||
infoNode?: ReactNode;
|
infoNode?: ReactNode;
|
||||||
} & { kcContext: KcContext.Template; } & KcTemplateProps;
|
} & { kcContext: KcContext.Template; } & KcTemplateProps;
|
||||||
|
|
||||||
|
|
||||||
export const Template = memo((props: TemplateProps) => {
|
export const Template = memo((props: TemplateProps) => {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -86,6 +85,7 @@ export const Template = memo((props: TemplateProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
let isUnmounted = false;
|
let isUnmounted = false;
|
||||||
|
const cleanups: (() => void)[] = [];
|
||||||
|
|
||||||
const toArr = (x: string | readonly string[] | undefined) =>
|
const toArr = (x: string | readonly string[] | undefined) =>
|
||||||
typeof x === "string" ? x.split(" ") : x ?? [];
|
typeof x === "string" ? x.split(" ") : x ?? [];
|
||||||
@ -114,13 +114,29 @@ export const Template = memo((props: TemplateProps) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (props.kcHtmlClass !== undefined) {
|
||||||
|
|
||||||
|
const htmlClassList =
|
||||||
document.getElementsByTagName("html")[0]
|
document.getElementsByTagName("html")[0]
|
||||||
.classList
|
.classList;
|
||||||
.add(cx(props.kcHtmlClass));
|
|
||||||
|
|
||||||
return () => { isUnmounted = true; };
|
const tokens = cx(props.kcHtmlClass).split(" ")
|
||||||
|
|
||||||
}, []);
|
htmlClassList.add(...tokens);
|
||||||
|
|
||||||
|
cleanups.push(() => htmlClassList.remove(...tokens));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
|
||||||
|
isUnmounted = true;
|
||||||
|
|
||||||
|
cleanups.forEach(f => f());
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}, [props.kcHtmlClass]);
|
||||||
|
|
||||||
if (!isExtraCssLoaded) {
|
if (!isExtraCssLoaded) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -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