A collection of server-agnostic OAuth 2.1 and OpenID Connect utilities
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.
Built on top of minimal dependencies:
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";
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);
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"orimport { buildUserInfo } from "unauth/oidc", to compose custom flows while keeping the same core primitives.
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);
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.
Published under the MIT license.
Made by community 💛
🤖 auto updated with automd
We use cookies
We use cookies to analyze traffic and improve your experience. You can accept or reject analytics cookies.