//unauthbysandros94

unauth

A collection of server-agnostic OAuth 2.1 and OpenID Connect utilities

1
0
1
1
TypeScript

unauth

npm version
npm downloads
bundle size

A collection of low-level, and high-level server-agnostic, OAuth 2.1 and OpenID Connect utilities based on JWT (unjwt). Adapters for popular frameworks are available (PRs are welcome for more!).

[!WARNING]
This package is in active development. It is not recommended for production use yet unless you are willing to help with testing and feedback.
Expect breaking changes, as I prioritize usability and correctness over stability at this stage.

Features

  • OAuth 2.1 core and extensions:
    • Authorization Code Grant (with PKCE)
    • Refresh Token Grant
    • Client Credentials Grant
    • JWT Bearer Token Grant (RFC 9068)
    • Token Introspection (RFC 7662) (planned in adapters)
    • Token Revocation (RFC 7009) (planned in adapters)
  • OpenID Connect Core 1.0:
    • ID Tokens
    • UserInfo Endpoint
    • Discovery Endpoint
  • Framework adapters:
    • H3 v1 (also for use with Nuxt, Nitro, etc.)
  • Works in Node.js, Deno, Bun and Browsers
  • Fully typed

Built on top of minimal dependencies:

Usage

Install the package:

# ✨ Auto-detect (supports npm, yarn, pnpm, deno and bun)
npx nypm install unauth

Import:

undefinedESM (Node.js, Bun, Deno)

// Main functions
import { OAuthProvider } from "unauth/oauth";
import { OIDCProvider } from "unauth/oidc";

undefinedCDN (Deno, Bun and Browsers)

// Main functions
import { OAuthProvider } from "https://esm.sh/unauth/oauth";
import { OIDCProvider } from "https://esm.sh/unauth/oidc";

Quick start (OIDC)

import { OIDCProvider } from "unauth/oidc";
import { generateJWK } from "unauth/utils";

// Configure the provider once during startup.
const [atJwk, idJwk] = await Promise.all([
  generateJWK("RS256", { kid: "at-rsa-1" }),
  generateJWK("RS256", { kid: "id-rsa-1" }),
]);
const oidc = useOIDCProvider({
  issuer: "https://auth.example.com",
  authorizationCodeOptions: {
    privateKey: "ac-secret",
  },
  refreshTokenOptions: {
    privateKey: "rt-secret",
  },
  accessTokenOptions: atJwk,
  idTokenOptions: idJwk,
});

// In your authorize endpoint
const authorize = oidc.validateAuthorizeRequest(req.query);
if (!authorize.success) {
  return redirectWithError(authorize.error);
}

const code = await oidc.issueAuthorizationCode({
  ...authorize.value,
  subject: "user-123",
  redirect_uri: authorize.value.redirect_uri ?? DEFAULT_REDIRECT_URI,
});

// In your token endpoint
const normalized = oidc.validateTokenRequest(req.body);
if (!normalized.success) {
  return normalized.error;
}

const grant = await oauth.issueTokenGrant(validation.value);
if (!grant.success) {
  return grant.error;
}

const idToken = await oidc.introspectIdToken(grant.value.id_token);

Quick start (OAuth only)

import { OAuthProvider } from "unauth/oauth";
import { generateJWK } from "unauth/utils";

// Configure the provider once during startup.
const atJwk = await generateJWK("RS256", { kid: "at-rsa-1" });
const oauth = useOAuthProvider({
  issuer: "https://auth.example.com",
  authorizationCodeOptions: {
    privateKey: "ac-secret",
  },
  refreshTokenOptions: {
    privateKey: "rt-secret",
  },
  accessTokenOptions: atJwk,
});

const validation = oauth.validateTokenRequest(req.body);
if (!validation.success) {
  return validation.error;
}

const grant = await oauth.issueTokenGrant(validation.value);
if (!grant.success) {
  return grant.error;
}

// Later, verify tokens issued by the provider
const accessClaims = await oauth.introspectAccessToken(
  grant.value.access_token,
);

[!NOTE]
For advanced use-cases you can import the lower-level helpers directly, e.g. import { issueAuthorizationCode } from "unauth/oauth" or import { buildUserInfo } from "unauth/oidc", to compose custom flows while keeping the same core primitives.

Adapters

  • undefinedH3 v1: For use with H3 v1

Minimal H3 v1 Example

In the following example instead of using useOIDCProvider or useOAuthProvider, we use createOIDCRouter (or createOAuthRouter) which creates an H3 router with all the necessary endpoints that can be mounted as a sub-app. We also provide an authorize hook to validate the client and redirect URI.

import {
  createApp,
  createRouter,
  defineEventHandler,
  getQuery,
  useBase,
} from "h3";
import { createOIDCRouter, validateRedirectUri } from "unauth/h3/oidc";
import { generateJWK } from "unauth";

const [atJwk, idJwk] = await Promise.all([
  generateJWK("RS256", { kid: "at-rsa-1" }),
  generateJWK("RS256", { kid: "id-rsa-1" }),
]);

// If the first argument is a string, it will return a handler with a base
const oidcRouter = createOIDCRouter({
  issuer: "http://localhost:3000",
  discovery: {
    // the base path where the OIDC endpoints will be served
    // (e.g. /oidc/v1/.well-known/openid-configuration)
    base: "/oidc/v1",
    // you can also override individual endpoints here, e.g.:
    // authorization_endpoint: "/oidc/v1/authorize",
  },

  authorizationCodeOptions: {
    privateKey: "ac-secret",
  },
  refreshTokenOptions: {
    privateKey: "rt-secret",
  },
  accessTokenOptions: atJwk, // we can directly pass keys and use default options
  idTokenOptions: idJwk, // same as accessTokenOptions

  // Hook that is called when the /authorize endpoint is hit
  authorize: async (input) => {
    // in a real app, you'd look up the client_id and allowed redirect URIs in your database
    if (input.client_id !== "test-client") {
      return {
        error: "invalid_client",
        error_description: "Unknown client",
      };
    }
    const validRedirectUri = validateRedirectUri(input.redirect_uri, [
      "http://localhost:3000/callback", // this is the one requested
      "http://localhost:3000/alt-callback",
    ]);
    if (!validRedirectUri.success) {
      return validRedirectUri.error;
    }

    // in a real app, you'd determine this from the user's login session
    const subject = "user-123";

    return {
      subject,
      redirect_uri: validRedirectUri.value,
    };
  },
});

// Create an H3 app instance
export const app = createApp();
const router = createRouter();

// Simple callback endpoint for manual testing; used by the scripted test (which intercepts the Location header)
router.get(
  "/callback",
  defineEventHandler((event) => {
    const q = getQuery<{ code?: string; state?: string }>(event);
    return `Callback received. code=${q.code ?? "<none>"} state=${q.state ?? "<none>"}`;
  }),
);

// Use the same base as used in `createOIDCRouter`
router.use("/oidc/v1/**", useBase("/oidc/v1", oidcRouter.handler));

app.use(router);

Development

local development
  • Clone this repository
  • Install latest LTS version of Node.js
  • Enable Corepack using corepack enable
  • Install dependencies using pnpm install
  • Run interactive tests using pnpm dev

Why unauth?

I started by building unjwt, as I needed a cryptographycally secure way to transmit sensitive information between various programming languages and servers. Not long after I started requiring some standardization, in particular on how to prepare and expect authorization data to be shared between parties (client and servers), but as I was testing various libraries I’ve never been satisfied by their DX (although most of them were great for someone that already knows the topic).
So I started building unauth as a collection of low-level primitives that then can be wrapped in higher-level abstractions, via adapters, to provide a “batteries included” experience while retaining control and flexibility of using your preferred storage, database and web frameworks.

Credits

License

Published under the MIT license.
Made by community 💛






🤖 auto updated with automd

[beta]v0.14.0