From 3044d16eb5a09e599e54a5ff717267dec5592295 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Mon, 9 Dec 2024 01:00:03 +0100 Subject: [PATCH] Add link to admin UI --- src/App.tsx | 443 ++++++++++++++++++++++++++++------------------------ src/oidc.ts | 28 +++- 2 files changed, 265 insertions(+), 206 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 789c115..fa29141 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useOidc, keycloakAccountUrl } from "oidc"; +import { useOidc, readRealm } from "oidc"; import { tss } from "tss-react/mui"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; @@ -78,223 +78,266 @@ export function App() { } export function ContextualizedApp() { + const { + logout, + goToAuthServer, + backFromAuthServer, + oidcTokens, + params: { clientId, issuerUri }, + } = useOidc(); - const { logout, goToAuthServer, backFromAuthServer, oidcTokens } = useOidc(); + const realm = readRealm({ issuerUri }); - const { classes } = useStyles(); + const { classes } = useStyles(); - return ( - <> -
-
- - You are now authenticated as {oidcTokens.decodedIdToken.preferred_username} - + return ( + <> +
+
+ + You are now authenticated as{" "} + {oidcTokens.decodedIdToken.preferred_username} + -
- -
- - Decoded JWT of the Open ID Connect ID token: - -
-                        {JSON.stringify(oidcTokens.decodedIdToken, null, 2)}
-                    
-
- -
+
+ +
+ + Decoded JWT of the Open ID Connect ID token: + +
{JSON.stringify(oidcTokens.decodedIdToken, null, 2)}
+
+ +
- - App-initiated actions - - - Actions that you can initiate from your App when the user is authenticated. - {" "}Learn more.
- They will redirect to pages of your login theme. (Not to the account theme) -
+ + App-initiated actions + + + Actions that you can initiate from your App when the user is + authenticated.{" "} + + Learn more + + .
+ They will redirect to the login UI. + (and not to the account UI) +
-
+
+
+ + {backFromAuthServer?.extraQueryParams.kc_action === + "UPDATE_PASSWORD" && + (() => { + switch (backFromAuthServer.result.kc_action_status) { + case "success": + return Password updated; + case "cancelled": + return ( + Password update cancelled + ); + } + })()} +
+
+ + {backFromAuthServer?.extraQueryParams.kc_action === + "UPDATE_PROFILE" && + (() => { + switch (backFromAuthServer.result.kc_action_status) { + case "success": + return Profile updated; + case "cancelled": + return Profile unchanged; + } + })()} +
+ + If this doesn't work on your Keycloak server it's because the + account deletion feature is disabled by default.{" "} + + See how to enable it. + {" "} + (Bottom of the documentation page) + + } + > + + +
+
+ +
-
- - {backFromAuthServer?.extraQueryParams.kc_action === "UPDATE_PASSWORD" && ( - (() => { - switch (backFromAuthServer.result.kc_action_status) { - case "success": - return ( - - Password updated - - ); - case "cancelled": - return ( - - Password update cancelled - - ); - } - })() - )} -
-
- - {backFromAuthServer?.extraQueryParams.kc_action === "UPDATE_PROFILE" && ( - (() => { - switch (backFromAuthServer.result.kc_action_status) { - case "success": - return ( - - Profile updated - - ); - case "cancelled": - return ( - - Profile unchanged - - ); - } - })() - )} -
- - If this doesn't work on your Keycloak server it's because the account deletion feature is disabled by default. - {" "} - See how to enable it. - (Bottom of the documentation page) - - }> - - + + Keycloak Account UI + + + Link to the Keycloak Account UI for the {realm} realm: + + + {(() => { + const url = `${issuerUri}/account?referrer=${clientId}&referrer_uri=${window.location.origin}`; -
+ return ( + + {url} + + ); + })()} + -
- -
+
+ +
- - Access to your account theme - - - If you are implementing an account theme, it's accessible at the following URL: - - - - {keycloakAccountUrl} - - + + Keycloak Admin UI + + + Link to the Keycloak Admin UI for the {realm} realm. + Note that user {oidcTokens.decodedIdToken.preferred_username} has to have the realm-admin role to access this page: + + + {(()=>{ -
- -
+ const url = `${issuerUri}/admin/${realm}/console`; -
- -
- - The source code of this test application is available at{" "} - - keycloakify/my-theme - - -
-
-
- + return ( + + {url} + + ); - ); + })()} + +
+ +
+ +
+ +
+ + The source code of this test application is available at{" "} + + keycloakify/my-theme + + +
+
+
+ + ); } -const useStyles = tss - .withName({ App }) - .create(({ theme }) => ({ - root: { - height: "100vh", - display: "flex", - justifyContent: "center", - alignItems: "center", - "& code": { - color: theme.palette.text.primary, - fontSize: "0.9em", - "&::before, &::after": { - content: `"\`"` - } - } - }, - content: { - backgroundColor: theme.palette.background.paper, - padding: theme.spacing(4), - borderRadius: theme.shape.borderRadius, - "&:hover": { - boxShadow: `inset 0 0 0.5px 1px hsla(0, 0%, +const useStyles = tss.withName({ App }).create(({ theme }) => ({ + root: { + height: "100vh", + display: "flex", + justifyContent: "center", + alignItems: "center", + "& code": { + color: theme.palette.text.primary, + fontSize: "0.9em", + "&::before, &::after": { + content: `"\`"`, + }, + }, + }, + content: { + backgroundColor: theme.palette.background.paper, + padding: theme.spacing(4), + borderRadius: theme.shape.borderRadius, + "&:hover": { + boxShadow: `inset 0 0 0.5px 1px hsla(0, 0%, 100%, 0.075), 0 0 0 1px hsla(0, 0%, 0%, 0.05), 0 0.3px 0.4px hsla(0, 0%, 0%, 0.02), 0 0.9px 1.5px hsla(0, 0%, 0%, 0.045), - 0 3.5px 6px hsla(0, 0%, 0%, 0.09);` - }, - - }, - decodedIdToken: { - border: `1px solid ${theme.palette.divider}`, - borderRadius: theme.shape.borderRadius - }, - appInitiatedActionsButtonsWrapper: { - display: "flex", - alignItems: "baseline", - gap: theme.spacing(2), - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2) - }, - appInitiatedActionButton: { - flex: 1, - display: "flex", - flexDirection: "column", - justifyContent: "center", - gap: theme.spacing(2) - }, - bottomWrapper: { - display: "flex", - justifyContent: "space-between", - alignItems: "end" - } - })); + 0 3.5px 6px hsla(0, 0%, 0%, 0.09);`, + }, + }, + decodedIdToken: { + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + }, + appInitiatedActionsButtonsWrapper: { + display: "flex", + alignItems: "baseline", + gap: theme.spacing(2), + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + appInitiatedActionButton: { + flex: 1, + display: "flex", + flexDirection: "column", + justifyContent: "center", + gap: theme.spacing(2), + }, + bottomWrapper: { + display: "flex", + justifyContent: "space-between", + alignItems: "end", + }, +})); diff --git a/src/oidc.ts b/src/oidc.ts index 10c4415..fdb5a89 100644 --- a/src/oidc.ts +++ b/src/oidc.ts @@ -1,8 +1,6 @@ import { createReactOidc } from "oidc-spa/react"; import { createMockReactOidc } from "oidc-spa/mock/react"; -let keycloakAccountUrl: string; - export const { OidcProvider, useOidc } = (() => { const isAuthGloballyRequired = true; @@ -11,8 +9,6 @@ export const { OidcProvider, useOidc } = (() => { if (import.meta.env.DEV) { - keycloakAccountUrl = `http://localhost:8080/realms/myream/account?referrer=myclient&referrer_uri=${window.location.origin}`; - return createMockReactOidc({ isUserInitiallyLoggedIn: true, isAuthGloballyRequired, @@ -42,6 +38,10 @@ export const { OidcProvider, useOidc } = (() => { email: "testuser@gmail.com" } }, + mockedParams: { + issuerUri: "http://localhost:8080/realms/myrealm", + clientId: "myclient" + } }); } @@ -84,7 +84,6 @@ export const { OidcProvider, useOidc } = (() => { })(); const clientId = getParam({ name: "client", defaultValue: "myclient" }); - keycloakAccountUrl = `${issuerUri}/account?referrer=${clientId}&referrer_uri=${window.location.origin}`; return createReactOidc({ issuerUri, @@ -99,7 +98,24 @@ export const { OidcProvider, useOidc } = (() => { })(); -export { keycloakAccountUrl }; +/** + * Extract last portion of the path + * Example "http://localhost:8080/realms/myrealm" => "myrealm" + */ +export function readRealm(params: { issuerUri: string }): string { + + const { issuerUri } = params; + + const match = issuerUri.match(/realms\/([^/]+)\/?/); + + if( match === null ){ + throw new Error(`Invalid issuerUri: ${issuerUri}`); + } + + return match[1]; + +} +