OpenIDConnect Strategy for Remix Auth

1.0.0 · active · verified Wed Apr 22

remix-auth-oidc is an authentication strategy for the Remix web framework, specifically designed to integrate with the remix-auth library to facilitate OpenID Connect (OIDC) authentication flows. It extends the existing OAuth2Strategy provided by remix-auth-oauth2, offering a robust foundation for OIDC providers like Keycloak. The current stable version is 1.0.0, indicating a stable API. While release cadence isn't explicitly stated, the library aligns with Remix's development and is actively maintained. Its key differentiator is its focus on OIDC, building upon the more general OAuth2 strategy to provide specific OIDC profile parsing and flow management, making it easier to integrate with identity providers that adhere strictly to OIDC specifications. It supports both Node.js and Cloudflare runtimes, making it versatile for various deployment environments. Developers commonly extend this base class to create specific strategies for their chosen OIDC providers.

Common errors

Warnings

Install

Imports

Quickstart

This quickstart demonstrates how to set up a Keycloak OIDC strategy using `remix-auth-oidc`, including environment variable configuration, user profile mapping, and integrating it with `remix-auth`'s Authenticator. It provides a complete, runnable example of how to define and use a custom OIDC strategy, including placeholder values for local development.

import { OIDCExtraParams, OIDCProfile, OpenIDConnectStrategy } from 'remix-auth-oidc';
import { Authenticator } from 'remix-auth';
import { createCookieSessionStorage } from '@remix-run/node';

// Mock user function for quickstart; replace with your actual user retrieval logic
async function getUser(accessToken: string, refreshToken: string, extraParams: OIDCExtraParams, profile: OIDCProfile, context: unknown) {
  console.log('User profile:', profile);
  // In a real app, you'd fetch/create a user from your DB here
  return {
    id: profile.id,
    name: profile.displayName || profile.emails?.[0]?.value || 'User',
    email: profile.emails?.[0]?.value || 'unknown@example.com',
    accessToken,
    refreshToken
  };
}

type KeycloakUserInfo = {
  sub: string,
  email: string,
  preferred_username?: string,
  name?: string,
  given_name?: string,
  family_name?: string,
  picture?: string
}

export type KeycloakUser = {
  id: string
  name?: string
  email: string
  accessToken: string
  refreshToken: string
}

export class KeycloakStrategy extends OpenIDConnectStrategy<KeycloakUser, OIDCProfile, OIDCExtraParams> {
  name = 'keycloak';

  constructor() {
    super(
      {
        authorizationURL: process.env.KEYCLOAK_TRUST_ISSUER + '/protocol/openid-connect/auth' ?? 'http://localhost:8080/realms/master/protocol/openid-connect/auth',
        tokenURL: process.env.KEYCLOAK_TRUST_ISSUER + '/protocol/openid-connect/token' ?? 'http://localhost:8080/realms/master/protocol/openid-connect/token',
        clientID: process.env.KEYCLOAK_CLIENT_ID ?? 'remix-app',
        clientSecret: process.env.KEYCLOAK_CLIENT_SECRET ?? 'your-client-secret',
        callbackURL: process.env.CALLBACK_URL ?? 'http://localhost:3000/auth/keycloak/callback'
      },
      async ({ accessToken, refreshToken, extraParams, profile, context }) => {
        return await getUser(
          accessToken,
          refreshToken,
          extraParams,
          profile,
          context
        );
      }
    )
  }

  protected async userProfile(accessToken: string, params: OIDCExtraParams): Promise<OIDCProfile> {
    const response = await fetch(
      `${process.env.KEYCLOAK_TRUST_ISSUER ?? 'http://localhost:8080/realms/master'}/protocol/openid-connect/userinfo`,
      {
        headers: {
          authorization: `Bearer ${accessToken}`,
        }
      }
    );
    if (!response.ok) {
      let body = await response.text();
      throw new Response(body, { status: 401 });
    }
    const data: KeycloakUserInfo = await response.json();
    return {
      provider: 'keycloak',
      id: data.sub,
      emails: [{ value: data.email }],
      displayName: data.name,
      name: {
        familyName: data.family_name,
        givenName: data.given_name,
      },
    }
  }
}

// Setup Authenticator and session storage
const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '__session',
    httpOnly: true,
    path: '/',
    sameSite: 'lax',
    secrets: [process.env.SESSION_SECRET ?? 's3cr3t'],
    secure: process.env.NODE_ENV === 'production',
  },
});

export const authenticator = new Authenticator<KeycloakUser>(sessionStorage);
authenticator.use(new KeycloakStrategy(), 'keycloak');

// Example of how you would initiate the authentication flow in a Remix action/loader
// (This part would be in a Remix route file, e.g., app/routes/auth.keycloak.tsx)
/*
import type { ActionFunctionArgs } from '@remix-run/node';
import { redirect } from '@remix-run/node';
import { authenticator } from '~/services/auth.server'; // Adjust path

export async function action({ request }: ActionFunctionArgs) {
  return authenticator.authenticate('keycloak', request, {
    successRedirect: '/dashboard',
    failureRedirect: '/login',
  });
}

// Example of callback route (e.g., app/routes/auth.keycloak.callback.tsx)
export async function loader({ request }: ActionFunctionArgs) {
  return authenticator.authenticate('keycloak', request, {
    successRedirect: '/dashboard',
    failureRedirect: '/login',
  });
}
*/

view raw JSON →