https://github.com/sandros94/unauth
A collection of server-agnostic OAuth 2.1 and OpenID Connect utilities
https://github.com/sandros94/unauth
oauth oidc
Last synced: 9 months ago
JSON representation
A collection of server-agnostic OAuth 2.1 and OpenID Connect utilities
- Host: GitHub
- URL: https://github.com/sandros94/unauth
- Owner: sandros94
- License: mit
- Created: 2025-08-24T21:50:22.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2025-10-02T01:52:06.000Z (9 months ago)
- Last Synced: 2025-10-02T03:27:24.203Z (9 months ago)
- Topics: oauth, oidc
- Language: TypeScript
- Homepage:
- Size: 703 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# unauth
[](https://npmjs.com/package/unauth)
[](https://npm.chart.dev/unauth)
[](https://bundlephobia.com/package/unauth)
A collection of low-level, and high-level server-agnostic, OAuth 2.1 and OpenID Connect utilities based on JWT ([`unjwt`](https://github.com/sandros94/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](https://v1.h3.dev) (also for use with Nuxt, Nitro, etc.)
- Works in Node.js, Deno, Bun and Browsers
- Fully typed
Built on top of minimal dependencies:
- [`unjwt`](https://github.com/sandros94/unjwt)
- [`unsecure`](https://github.com/sandros94/unsecure)
## Usage
Install the package:
```sh
# ✨ Auto-detect (supports npm, yarn, pnpm, deno and bun)
npx nypm install unauth
```
Import:
**ESM** (Node.js, Bun, Deno)
```js
// Main functions
import { OAuthProvider } from "unauth/oauth";
import { OIDCProvider } from "unauth/oidc";
```
**CDN** (Deno, Bun and Browsers)
```js
// Main functions
import { OAuthProvider } from "https://esm.sh/unauth/oauth";
import { OIDCProvider } from "https://esm.sh/unauth/oidc";
```
### Quick start (OIDC)
```ts
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)
```ts
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
- **H3 v1**: For use with [H3 v1](https://v1.h3.dev)
#### 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.
```ts
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 ?? ""} state=${q.state ?? ""}`;
}),
);
// 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](https://nodejs.org/en/)
- Enable [Corepack](https://github.com/nodejs/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
- Thanks to [Vidbase, Inc.](https://github.com/vidbase) (in particular to [Van Nguyen](https://github.com/thegoleffect)) for the npm package name donation
## License
Published under the [MIT](https://github.com/sandros94/unauth/blob/main/LICENSE) license.
Made by [community](https://github.com/sandros94/unauth/graphs/contributors) 💛
---
_🤖 auto updated with [automd](https://automd.unjs.io)_