OIDC SPA
GitHubHome
v6
  • Documentation
  • Release Notes & Upgrade Instructions
v6
  • Installation
  • Basic Usage
  • Web API
  • Auto Login
  • Auto Logout
  • Error Management
  • Mock
  • User Account Management
  • User Session Initialization
  • Tokens Renewal
  • Setup Guides
    • React Router
    • TanStack Router
    • Full-Stack with Node REST API
  • Providers Configuration
    • Keycloak
    • Auth0
    • Microsoft Entra ID
    • Google OAuth 2.0
    • Other OIDC Provider
  • Resources
    • Why No Client Secret?
    • End of third-party cookies
    • JWT Of the Access Token
    • Discord Server
  • User Impersonation
  • Sponsors
Powered by GitBook
On this page
  • Client Side
  • Server Side
  • Testable example

Was this helpful?

Export as PDF

Web API

The primary usecase for a library like oidc-spa is to use it to authenticate against a REST, tRPC, or Websocket API.

Client Side

Let's see a very basic REST API example:

Initialize oidc-spa and expose the oidc instance as a promise:

src/oidc.ts
import { createOidc } from "oidc-spa";

export const prOidc = createOidc({/* ... */});

Create a REST API Client that adds the OIDC Access Token as Autorization header to every HTTP request:

src/api.ts
import axios from "axios";
import { prOidc } 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 prOidc;

    if( !oidc.isUserLoggedIn ){
        throw new Error("We made a logic error: If the user isn't logged in we shouldn't be making request to an API endpoint that requires authentication");
    }
    
    // 99.9% of the times you'll get the token imediately.
    // The 0.1% is after the computer wakes up from sleep.
    const { accessToken } = await oidc.getTokens_next();
    
    config.headers.Authorization = `Bearer ${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)
};
src/oidc.ts
import { createReactOidc } from "oidc-spa/react";

export const { 
    OidcProvider, 
    useOidc,
    getOidc
} = createReactOidc(/* ... */);

Create a REST API Client that adds the OIDC Access Token as Autorization header to every HTTP request:

src/api.ts
import 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");
    }
    
    const { accessToken } = await oidc.getTokens();
    
    config.headers.Authorization = `Bearer ${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)
};

Using your REST API client in your REACT components:

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>
    );

}

Server Side

If you're implementing a JavaScript Backend (Node/Deno/webworker) oidc-spa also exposes an utility to help you validate and decode the access token that your client sends in the authorization header. Granted, this is fully optional feel free to use anything else. Let's assume we have a Node.js REST API build with Express or Hono. You can create an oidc file as such:

src/oidc.ts
import { createOidcBackend } from "oidc-spa/backend";
import { z } from "zod";
import { HTTPException } from "hono/http-exception";

const zDecodedAccessToken = z.object({
    sub: z.string(),
    aud: z.union([z.string(), z.array(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()
});

export type DecodedAccessToken = z.infer<typeof zDecodedAccessToken>;

export async function createDecodeAccessToken(params: { 
    issuerUri: string;
    audience: string 
}) {
    const { issuerUri, audience } = params;

    const { verifyAndDecodeAccessToken } = await createOidcBackend({
        issuerUri,
        decodedAccessTokenSchema: zDecodedAccessToken
    });

    function decodeAccessToken(params: {
        authorizationHeaderValue: string | undefined;
        requiredRole?: string;
    }): DecodedAccessToken {
        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.realm_access.roles.includes(requiredRole)) {
            throw new HTTPException(401);
        }

        {
            const { aud } = decodedAccessToken;

            const aud_array = typeof aud === "string" ? [aud] : aud;

            if (!aud_array.includes(audience)) {
                throw new HTTPException(401);
            }
        }

        return decodedAccessToken;
    }

    return { decodeAccessToken };
}

Then you can enforce that some endpoints of your API requires the user to be authenticated, in this example we use Hono:

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({
        // Here example with Keycloak but it work the same with 
        // any provider as long as the access token is a JWT.
        issuerUri: "https://auth.my-company.com/realms/myrealm",
        audience: "account" // default audience in Keycloak
    });

    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")
            });

            const todos = await 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`)

})();

Testable example

PreviousBasic UsageNextAuto Login

Last updated 1 month ago

Was this helpful?

This example is purposefully very basic to minimize noise but in your App you might want to consider using solutions like (if you have JS backend) and .

tRPC
TanStack Query
Full-Stack with Node REST API