Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
undefinednpm install --save oidc-spayarn add oidc-spapnpm add oidc-spabun add oidc-spa<html>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>Let's get your App authenticated!
import { createReactOidc } from "oidc-spa/react";
export const {
OidcProvider,
useOidc,
getOidc
} = createReactOidc(/* ... */);import { createOidc } from "oidc-spa";
const oidc = await createOidc({
issuerUri: "https://auth.your-domain.net/realms/myrealm",
clientId: "myclient",
/**
* Vite: `publicUrl: import.meta.env.BASE_URL`
* CRA: `publicUrl: process.env.PUBLIC_URL`
* Other: `publicUrl: "/"` (Usually)
*/
publicUrl: import.meta.env.BASE_URL
});
if (!oidc.isUserLoggedIn) {
// The user is not logged in.
// We can call login() to redirect the user to the login/register page.
// This return a promise that never resolve.
oidc.login({
/**
* If you are calling login() in the callback of a click event
* set this to false.
*/
doesCurrentHrefRequiresAuth: false
/**
* Optionally, you can add some extra parameter
* to be added on the login url.
* (Can also be a parameter of createOidc `extraQueryParams: ()=> ({ ui_locales: "fr" })`)
*/
//extraQueryParams: { kc_idp_hint: "google", ui_locales: "fr" }
/**
* You can allso set where to redirect the user after
* successful login
*/
// redirectUrl: "/dashboard"
/**
* Keycloak: You can also send the users directly to the register page
* see: https://github.com/keycloakify/oidc-spa/blob/14a3777601c50fa69d1221495d77668e97443119/examples/tanstack-router-file-based/src/components/Header.tsx#L54-L66
*/
});
} else {
// The user is logged in.
const {
// The accessToken is what you'll use as a Bearer token to
// authenticate to your APIs
accessToken,
decodedIdToken
} = oidc.getTokens();
fetch("https://api.your-domain.net/orders", {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
.then(response => response.json())
.then(orders => console.log(orders));
// To call when the user click on logout.
// You can also redirect to a custom url with
// { redirectTo: "specific url", url: "/bye" }
oidc.logout({ redirectTo: "home" });
// If you are wondering why ther's a decodedIdToken and no
// decodedAccessToken read this: https://docs.oidc-spa.dev/resources/jwt-of-the-access-token
console.log(`Hello ${decodedIdToken.preferred_username}`);
// Note that in this example the decodedIdToken is not typed.
// What is inside the idToken is defined by the OIDC server you are using.
// If you want to specify the type of the decodedIdToken you can do:
//
// import { z } from "zod";
// export const { useOidc } = createUseOidc({
// ...
// decodedIdTokenSchema: z.object({
// sub: z.string(),
// preferred_username: z.string(),
// // ... other properties
// })
// })
}import { createReactOidc } from "oidc-spa/react";
export const { OidcProvider, useOidc, getOidc } = createReactOidc({
// NOTE: If you don't have the params right away see note below.
issuerUri: "https://auth.your-domain.net/realms/myrealm",
clientId: "myclient",
/**
* Vite: `publicUrl: import.meta.env.BASE_URL`
* CRA: `publicUrl: process.env.PUBLIC_URL`
* Other: `publicUrl: "/"` (Usually)
*/
publicUrl: import.meta.env.BASE_URL
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<OidcProvider
// Optional, it's usually so fast that a fallback is really not required.
fallback={<>Checking authentication ⌛️</>}
>
<App />
</OidcProvider>
);
function App() {
const { isUserLoggedIn, login, logout, oidcTokens } = useOidc();
return (
isUserLoggedIn ? (
<>
{/*
Note: The decodedIdToken can be typed and validated with zod See: https://github.com/keycloakify/oidc-spa/blob/fddac99d2b49669a376f9a0b998a8954174d195e/examples/tanstack-router/src/oidc.tsx#L17-L43
If you are wondering why ther's a decodedIdToken and no
decodedAccessToken read this: https://docs.oidc-spa.dev/resources/jwt-of-the-access-token
*/}
<span>Hello {oidcTokens.decodedIdToken.preferred_username}</span>
<button onClick={() => logout({ redirectTo: "home" })}>
Logout
</button>
</>
) : (
<button onClick={() => login({
/**
* If you are calling login() in the callback of a button click
* (like here) set this to false.
*/
doesCurrentHrefRequiresAuth: false
/**
* Optionally, you can add some extra parameter
* to be added on the login url.
* (Can also be a parameter of createReactOidc `extraQueryParams: ()=> ({ ui_locales: "fr" })`)
*/
//extraQueryParams: { kc_idp_hint: "google", ui_locales: "fr" }
/**
* You can allso set where to redirect the user after
* successful login
*/
// redirectUrl: "/dashboard"
/**
* Keycloak: You can also send the user direcly to the register page
* See: https://github.com/keycloakify/oidc-spa/blob/14a3777601c50fa69d1221495d77668e97443119/examples/tanstack-router-file-based/src/components/Header.tsx#L54-L66
*/
})} >
Login
</button>
)
);
}
import { useEffect, useState } from "react";
type Order = {
id: number;
name: string;
};
function OrderHistory(){
const { oidcTokens } = useOidc({ assertUserLoggedIn: true });
const [orders, setOrders] = useState<Order[] | undefined>(undefined);
useEffect(
()=> {
fetch("https://api.your-domain.net/orders", {
headers: {
Authorization: `Bearer ${oidcTokens.accessToken}`
}
})
.then(response => response.json())
.then(orders => setOrders(orders));
},
[]
);
if(orders === undefined){
return <>Loading orders ⌛️</>
}
return (
<ul>
{orders.map(order => (
<li key={order.id}>{order.name}</li>
))}
</ul>
);
}export const {
OidcProvide,
useOidc,
getOidc
} = createReactOidc(async ()=> {
const {
issuerUri,
clientId
} = await axios.get("/oidc-params").then(r => r.data);
return {
issuerUri,
clientId,
publicUrl: import.meta.env.BASE_URL
};
});git clone https://github.com/keycloakify/oidc-spa
mv oidc-spa/examples/react-router oidc-spa-react-router
rm -rf oidc-spa
cd oidc-spa-react-router
yarn
yarn devimport axios from "axios";
import { getOidc } from "oidc";
type Api = {
getTodos: () => Promise<{ id: number; title: string; }[]>;
addTodo: (todo: { title: string; }) => Promise<void>;
};
const axiosInstance = axios.create({ baseURL: import.meta.env.API_URL });
axiosInstance.interceptors.request.use(async config => {
const oidc= await getOidc();
if( !oidc.isUserLoggedIn ){
throw new Error("We made a logic error: The user should be logged in at this point");
}
config.headers.Authorization = `Bearer ${oidc.getTokens().accessToken}`;
return config;
});
export const api: Api = {
getTodo: ()=> axiosInstance.get("/todo").then(response => response.data),
addTodo: todo => axiosInstance.post("/todo", todo).then(response => response.data)
};import { api } from "../api";
type Todo= {
id: number;
title: string;
};
function UserTodos(){
const [todos, setTodos] = useState<Todo[] | undefined>(undefined);
useEffect(
()=> {
api.getTodos().then(todos => setTodos(todos));
},
[]
);
if(todos === undefined){
return <>Loading your todos items ⌛️</>
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}import { createOidcBackend } from "oidc-spa/backend";
import { z } from "zod";
import { HTTPException } from "hono/http-exception";
export async function createDecodeAccessToken() {
const oidcIssuerUri = process.env.OIDC_ISSUER
if (oidcIssuerUri === undefined) {
throw new Error("OIDC_ISSUER must be defined in the environment variables")
}
const { verifyAndDecodeAccessToken } = await createOidcBackend({
issuerUri: oidcIssuerUri,
decodedAccessTokenSchema: z.object({
sub: z.string(),
realm_access: z.object({
roles: z.array(z.string())
})
// Some other info you might want to read from the accessToken, example:
// preferred_username: z.string()
})
});
function decodeAccessToken(params: {
authorizationHeaderValue: string | undefined;
requiredRole?: string;
}) {
const { authorizationHeaderValue, requiredRole } = params;
if( authorizationHeaderValue === undefined ){
throw new HTTPException(401);
}
const result = verifyAndDecodeAccessToken({
accessToken: authorizationHeaderValue.replace(/^Bearer /, "")
});
if( !result.isValid ){
switch( result.errorCase ){
case "does not respect schema":
throw new Error(`The access token does not respect the schema ${result.errorMessage}`);
case "invalid signature":
case "expired":
throw new HTTPException(401);
}
}
const { decodedAccessToken } = result;
if(
requiredRole !== undefined &&
!decodedAccessToken.ream_access.roles.includes(requiredRole)
){
throw new HTTPException(401);
}
return decodedAccessToken;
}
return { decodeAccessToken };
}import { z, createRoute, OpenAPIHono } from "@hono/zod-openapi";
import { serve } from "@hono/node-server"
import { HTTPException } from "hono/http-exception";
import { getUserTodoStore } from "./todo";
import { createDecodeAccessToken } from "./oidc";
(async function main() {
const { decodeAccessToken } = await createDecodeAccessToken();
const app = new OpenAPIHono();
{
const route = createRoute({
method: 'get',
path: '/todos',
responses: {/* ... */}
});
app.openapi(route, async c => {
const decodedAccessToken = decodeAccessToken({
authorizationHeaderValue: c.req.header("Authorization")
});
if (decodedAccessToken === undefined) {
throw new HTTPException(401);
}
const todos = getUserTodoStore(decodedAccessToken.sub).getAll();
return c.json(todos);
});
}
const port = parseInt(process.env.PORT);
serve({
fetch: app.fetch,
port
})
console.log(`\nServer running. OpenAPI documentation available at http://localhost:${port}/doc`)
})();And why it's not supposed to be read on the client side.
import { decodeJwt } from "oidc-spa/tools/decodeJwt";
const decodedAccessToken = decodeJwt(oidc.getTokens().accessToken);Vite + TypeScript + React + Tanstack Router
git clone https://github.com/keycloakify/oidc-spa
mv oidc-spa/examples/tanstack-router-file-based oidc-spa-tanstack-router
rm -rf oidc-spa
cd oidc-spa-tanstack-router
yarn
yarn devFriend of the project
import { createOidc } from "oidc-spa";
const oidc = await createOidc({ ... });
if( oidc.isUserLoggedIn ){
oidc.renewToken();
}import { createOidc } from "oidc-spa";
const oidc = await createOidc({ ... });
if( !oidc.isUserLoggedIn ){
oidc.subscribeToTokensChange(() => {
console.log("Tokens change", oidc.getTokens());
});
}import { createReactOidc } from "oidc-spa/react";
export const {
/* ... */
getOidc
} = createReactOidc({ /* ... */ });
getOidc().then(oidc => {
if( !oidc.isUserLoggedIn ){
return;
}
oidc.subscribeToTokensChange(() => {
console.log("Tokens change", oidc.getTokens());
});
});import { useOidc } from "oidc";
import { useEffect } from "react";
function MyComponent(){
const { renewTokens } = useOidc({ assertUserLoggedIn: true });
useEffect(()=> {
renewTokens();
}, []);
return <>...</>;
}import { useOidc } from "./oidc";
export function PotectedPage() {
const { oidcTokens } = useOidc({ assertUserLoggedIn: true});
useEffect(()=> {
console.log("Tokens changed", oidcTokens);
}, [oidcTokens]);
// ...
}import { createOidc, OidcInitializationError } from "oidc-spa";
try{
const oidc = await createOidc({
// ...
isAuthGloballyRequired: true,
// Optional, the default value is: location.href (here)
// postLoginRedirectUrl: "/dashboard"
});
}catch(error){
const oidcInitializationError = error as OidcInitializationError;
console.log(oidcInitializationError.message);
console.log(oidcInitializationError.type); // "server down" | "bad configuration" | "unknown";
}import { createReactOidc } from "oidc-spa/react";
export const {
OidcProvider,
useOidc,
prOidc
} = createReactOidc({
// ...
isAuthGloballyRequired: true,
// Optional, the default value is: location.href (here)
// postLoginRedirectUrl: "/dashboard"
});
import { OidcProvider } from "oidc";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<OidcProvider
ErrorFallback={({ initializationError })=>(
<h1 style={{ color: "red" }}>
An error occurred while initializing the OIDC client:
{initializationError.message}
{initializationError.type} /* "server down" | "bad configuration" | "unknown"; */
</h1>
)}
>
{/* ... */}
</OidcProvider>
</React.StrictMode>
);
import { createOidc } from "oidc-spa";
import { createReactOidc } from
import { useOidc } from "./oidc";
import { decodeJwt } from "oidc-spa/tools/decodeJwt";
const { oidcTokens } = useOidc();
const decodedAccessToken = decodeJwt(oidcTokens.accessToken);import { createOidc } from "oidc-spa";
import { createMockOidc } from "oidc-spa/mock";
import { z } from "zod";
const decodedIdTokenSchema = z.object({
sub: z.string(),
preferred_username: z.string()
});
const publicUrl= import.meta.env.BASE_URL;
const oidc = !import.meta.env.VITE_OIDC_ISSUER
? createMockOidc({
isUserInitiallyLoggedIn: false,
publicUrl,
mockedTokens: {
decodedIdToken: {
sub: "123",
preferred_username: "john doe"
} satisfies z.infer<typeof decodedIdTokenSchema>
}
})
: await createOidc({
issuerUri: import.meta.env.VITE_OIDC_ISSUER,
clientId: import.meta.env.VITE_OIDC_CLIENT_ID,
publicUrl
});import { createReactOidc } from "oidc-spa/react";
import { createMockReactOidc } from "oidc-spa/mock/react";
import { z } from "zod";
const decodedIdTokenSchema = z.object({
sub: z.string(),
preferred_username: z.string()
});
const publicUrl = import.meta.env.BASE_URL;
export const { OidcProvider, useOidc, prOidc } =
!import.meta.env.VITE_OIDC_ISSUER ?
createMockReactOidc({
isUserInitiallyLoggedIn: false,
publicUrl,
mockedTokens: {
decodedIdToken: {
sub: "123",
preferred_username: "john doe"
} satisfies z.infer<typeof decodedIdTokenSchema>
}
}) :
createReactOidc({
issuerUri: import.meta.env.VITE_OIDC_ISSUER,
clientId: import.meta.env.VITE_OIDC_CLIENT_ID,
publicUrl,
decodedIdTokenSchema
});import { createOidc } from "oidc-spa";
import { createOidc } from "oidc-spa";
import { createOidc } from "oidc-spa";
const oidc = await createOidc({
// ...
__unsafe_ssoSessionIdleSeconds: 10 * 60 // 10 minutes
//autoLogoutParams: { redirectTo: "current page" } // Default
//autoLogoutParams: { redirectTo: "home" }
//autoLogoutParams: { redirectTo: "specific url", url: "/a-page" }
});import { createReactOidc } from "oidc-spa/react";
export const {
OidcProvider,
useOidc
} = createReactOidc({
// ...
__unsafe_ssoSessionIdleSeconds: 10 * 60 // Ten minutes
//autoLogoutParams: { redirectTo: "current page" } // Default
//autoLogoutParams: { redirectTo: "home" }
//autoLogoutParams: { redirectTo: "specific url", url: "/a-page" }
});import { useOidc } from "oidc";
import { useEffect } from "react";
function LoginButton() {
const { isUserLoggedIn, login, initializationError } = useOidc();
useEffect(() => {
if (!initializationError) {
return;
}
console.warn("OIDC initialization error");
switch (initializationError.type) {
case "bad configuration":
console.warn("The identity server and/or the client is misconfigured");
break;
case "server down":
console.warn("The identity server is down");
break;
case "unknown":
console.warn("An unknown error occurred");
break;
}
}, []);
if (isUserLoggedIn) {
return null;
}
return (
<button onClick={() => {
if (initializationError) {
alert(`Can't login now, try again later: ${
initializationError.message
}`);
return;
}
login({ ... });
}}>
Login
</button>
);
}function ProtectedPage() {
// Here we can safely assume that the user is logged in.
const { goToAuthServer, backFromAuthServer } = useOidc({ assertUserLoggedIn: true });
return (
<>
<button
onClick={() =>
goToAuthServer({
extraQueryParams: { kc_action: "UPDATE_PASSWORD" }
})
}
>
Change password
</button>
{/*
Optionally you can display a feedback message to the user when they
are redirected back to the app after completing or canceling the
action.
*/}
{backFromAuthServer?.extraQueryParams.kc_action === "UPDATE_PASSWORD" && (
<p>
{(()=>{
switch(backFromAuthServer.result.kc_action_status){
case "success":
return "Password sucessfully updated";
case "cancelled":
return "Password unchanged";
}
})()}
</p>
)}
</>
);
}
const { unsubscribeFromAutoLogoutCountdown } = oidc.subscribeToAutoLogoutCountdown(
({ secondsLeft }) => {
if( secondsLeft === undefined ){
console.log("Countdown reset, the user moved");
return;
}
if( secondsLeft > 60 ){
return;
}
console.log(`${secondsLeft} before auto logout`)
}
); issuerUri: "https://auth.your-domain.net/realms/myrealm",
clientId: "myclient"