diff --git a/README.md b/README.md index 22ef4a9..4fe4b0f 100644 --- a/README.md +++ b/README.md @@ -220,4 +220,4 @@ jobs: EOF ``` -You can also remove `oidc-spa`, `powerhooks` and `tsafe` from your dependencies. +You can also remove `oidc-spa`, `powerhooks`, `zod` and `tsafe` from your dependencies. diff --git a/package.json b/package.json index 3ac061e..d6037e7 100755 --- a/package.json +++ b/package.json @@ -25,12 +25,13 @@ "keywords": [], "dependencies": { "evt": "^2.4.15", - "oidc-spa": "^2.2.0", - "keycloakify": "^9.1.4", + "oidc-spa": "^3.0.3", + "keycloakify": "^9.1.6", "powerhooks": "^0.26.8", "react": "18.1.0", "react-dom": "18.1.0", - "tsafe": "^1.6.0" + "tsafe": "^1.6.0", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^15.3.1", diff --git a/src/App/App.tsx b/src/App/App.tsx index ed71f44..13633b8 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,44 +1,7 @@ import "./App.css"; import logo from "./logo.svg"; import myimg from "./myimg.png"; -import { useMemo } from "react"; -import { createOidcProvider, useOidc } from "oidc-spa/react"; -import { decodeJwt } from "oidc-spa"; -import { assert } from "tsafe/assert"; - -//On older Keycloak version you need the /auth (e.g: http://localhost:8080/auth) -//On newer version you must remove it (e.g: http://localhost:8080 ) -const keycloakUrl = "https://auth.code.gouv.fr/auth"; -const keycloakRealm = "keycloakify"; -const keycloakClientId= "starter"; - -const { OidcProvider } = createOidcProvider({ - issuerUri: `${keycloakUrl}/realms/${keycloakRealm}`, - clientId: keycloakClientId, - // NOTE: You can also pass queries params when calling oidc.login() - getExtraQueryParams: () => ({ - // This adding ui_locales to the url will ensure the consistency of the language between the app and the login pages - // If your app implements a i18n system (like i18nifty.dev for example) you should use this and replace "en" by the - // current language of the app. - // On the other side you will find kcContext.locale.currentLanguageTag to be whatever you set here. - "ui_locales": "en", - "my_custom_param": "value of foo transferred to login page" - }), - /* - * This parameter have to be provided provide if your App is not hosted at the origin of the subdomain. - * For example if your site is hosted by navigating to `https://www.example.com` - * you don't have to provide this parameter. - * On the other end if your site is hosted by navigating to `https://www.example.com/my-app` - * Then you want to set publicUrl to `/my-app` - * - * Be mindful that `${window.location.origin}${publicUrl}/silent-sso.html` must return the `silent-sso.html` that - * you are supposed to have created in your `public/` directory. - * - * If your are still using `create-react-app` (like we are for now) you can just set - * publicUrl to `process.env.PUBLIC_URL` and don't have to think about it further. - */ - publicUrl: process.env.PUBLIC_URL -}); +import { OidcProvider, useOidc, getKeycloakAccountUrl } from "./oidc"; export default function App() { return ( @@ -48,25 +11,44 @@ export default function App() { ); } - function ContextualizedApp() { - const { oidc } = useOidc(); + const { isUserLoggedIn, login, logout, oidcTokens } = useOidc(); return (
- { - oidc.isUserLoggedIn ? - oidc.logout({ redirectTo: "home" })} /> - : - +
+                                {JSON.stringify(oidcTokens.decodedIdToken, null, 2)}
+                            
+ + ) + : + ( + + ) } logo test_image @@ -79,67 +61,3 @@ function ContextualizedApp() { } -function AuthenticatedRoute(props: { logout: () => void; }) { - - const { logout } = props; - - const { user } = useUser(); - - return ( - <> -

Hello {user.name} !

- Link to your Keycloak account - -
{JSON.stringify(user, null, 2)}
- - ); - -} - -function useUser() { - const { oidc } = useOidc(); - - assert(oidc.isUserLoggedIn, "This hook can only be used when the user is logged in"); - - const { idToken } = oidc.getTokens(); - - const user = useMemo( - () => - decodeJwt<{ - // Use https://jwt.io/ to tell what's in your idToken - // It will depend of your Keycloak configuration. - // Here I declare only two field on the type but actually there are - // Many more things available. - sub: string; - name: string; - preferred_username: string; - // This is a custom attribute set up in our Keycloak configuration - // it's not present by default. - // See https://docs.keycloakify.dev/realtime-input-validation#getting-your-custom-user-attribute-to-be-included-in-the-jwt - favorite_pet: "cat" | "dog" | "bird"; - }>(idToken), - [idToken] - ); - - return { user }; -} - -function buildAccountUrl( - params: { - locale: string; - } -){ - const { locale } = params; - - const accountUrl = new URL(`${keycloakUrl}/realms/${keycloakRealm}/account`); - - const searchParams = new URLSearchParams(); - - searchParams.append("kc_locale", locale); - searchParams.append("referrer", keycloakClientId); - searchParams.append("referrer_uri", window.location.href); - - accountUrl.search = searchParams.toString(); - - return accountUrl.toString(); -} \ No newline at end of file diff --git a/src/App/oidc.ts b/src/App/oidc.ts new file mode 100644 index 0000000..c52f749 --- /dev/null +++ b/src/App/oidc.ts @@ -0,0 +1,63 @@ +// See documentation of oidc-spa for more details: +// https://docs.oidc-spa.dev + +import { createOidcProvider, createUseOidc } from "oidc-spa/react"; +import { z } from "zod"; + +//On older Keycloak version you need the /auth (e.g: http://localhost:8080/auth) +//On newer version you must remove it (e.g: http://localhost:8080 ) +const keycloakUrl = "https://auth.code.gouv.fr/auth"; +const keycloakRealm = "keycloakify"; +const keycloakClientId= "starter"; + +export const { OidcProvider } = createOidcProvider({ + issuerUri: `${keycloakUrl}/realms/${keycloakRealm}`, + clientId: keycloakClientId, + // NOTE: You can also pass queries params when calling oidc.login() + getExtraQueryParams: () => ({ + // This adding ui_locales to the url will ensure the consistency of the language between the app and the login pages + // If your app implements a i18n system (like i18nifty.dev for example) you should use this and replace "en" by the + // current language of the app. + // On the other side you will find kcContext.locale.currentLanguageTag to be whatever you set here. + "ui_locales": "en", + "my_custom_param": "value of foo transferred to login page" + }), + publicUrl: process.env.PUBLIC_URL +}); + +export const { useOidc } = createUseOidc({ + decodedIdTokenSchema: z.object({ + // Use https://jwt.io/ to tell what's in your idToken + // It will depend of your Keycloak configuration. + // Here I declare only two field on the type but actually there are + // Many more things available. + sub: z.string(), + name: z.string(), + preferred_username: z.string(), + // This is a custom attribute set up in our Keycloak configuration + // it's not present by default. + // See https://docs.keycloakify.dev/realtime-input-validation#getting-your-custom-user-attribute-to-be-included-in-the-jwt + favorite_pet: z.union([z.literal("cat"), z.literal("dog"), z.literal("bird")]) + }) +}); + + +export function getKeycloakAccountUrl( + params: { + locale: string; + } +){ + const { locale } = params; + + const accountUrl = new URL(`${keycloakUrl}/realms/${keycloakRealm}/account`); + + const searchParams = new URLSearchParams(); + + searchParams.append("kc_locale", locale); + searchParams.append("referrer", keycloakClientId); + searchParams.append("referrer_uri", window.location.href); + + accountUrl.search = searchParams.toString(); + + return accountUrl.toString(); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ab243d4..838ae17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9760,10 +9760,10 @@ jwt-decode@^3.1.2: resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== -keycloakify@^9.1.4: - version "9.1.4" - resolved "https://registry.yarnpkg.com/keycloakify/-/keycloakify-9.1.4.tgz#87c864e3ca12e3b0872cbd7ca1b8a0cebbab622d" - integrity sha512-jb8r2pkAk2gyhieS106ytQbz8ygSRA1uETYE3s5uHYRF08ERMvDgwSbIDMFvleWYnd5saXXhEjL/4wPFXBUDLQ== +keycloakify@^9.1.6: + version "9.1.6" + resolved "https://registry.yarnpkg.com/keycloakify/-/keycloakify-9.1.6.tgz#2d7a633e7727c44afaeec6b2ef98956540097db4" + integrity sha512-OXmW47gGtrtBVolYi/VqMOAxHEAl0tnm1sWaVxFBJQHxAMgX7soV8tTF/fvPwEQpi+BshBQe+wgCXCVfqzKgkA== dependencies: "@babel/generator" "^7.22.9" "@babel/parser" "^7.22.7" @@ -10917,10 +10917,10 @@ oidc-client-ts@^2.3.0: crypto-js "^4.1.1" jwt-decode "^3.1.2" -oidc-spa@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/oidc-spa/-/oidc-spa-2.2.0.tgz#7b8f6a149e5320d6a5275183a2026266916cbf9a" - integrity sha512-aOVMktCS2gQC+5T2OIu446dRHgzz69Ezec0MfxYS3G1uP21wSr/1z3a5r3fFDcoxNAc3fOKesETbztKezFDd5w== +oidc-spa@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/oidc-spa/-/oidc-spa-3.0.3.tgz#01e524064e91615e3f2c6d6c906234fc13351787" + integrity sha512-yoAQHaTPw0Eb3AYLQ1ODuCfO0iboUv3agnz45QSm0LWG9o6+9mtN6g71XljrSIPR/G5g4rLria4Ke+Rq/LoJZg== dependencies: jwt-decode "^3.1.2" oidc-client-ts "^2.3.0" @@ -15555,6 +15555,11 @@ zod@^3.17.10: resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== +zod@^3.22.4: + version "3.22.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" + integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"