mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
feat: Sync to OSS repo
This commit is contained in:
173
packages/auth/src/provider/oidc.ts
Normal file
173
packages/auth/src/provider/oidc.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Use this to connect authentication providers that support OIDC.
|
||||
*
|
||||
* ```ts {5-8}
|
||||
* import { OidcProvider } from "@openauthjs/openauth/provider/oidc"
|
||||
*
|
||||
* export default issuer({
|
||||
* providers: {
|
||||
* oauth2: OidcProvider({
|
||||
* clientId: "1234567890",
|
||||
* issuer: "https://auth.myserver.com"
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { JWTPayload } from 'hono/utils/jwt/types';
|
||||
import { createLocalJWKSet, JSONWebKeySet, jwtVerify } from 'jose';
|
||||
|
||||
import { WellKnown } from '../client.js';
|
||||
import { OauthError } from '../error.js';
|
||||
import { getRelativeUrl, lazy } from '../util.js';
|
||||
import { Provider } from './provider.js';
|
||||
|
||||
export interface OidcConfig {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* The client ID.
|
||||
*
|
||||
* This is just a string to identify your app.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* clientID: "my-client"
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
clientID: string;
|
||||
/**
|
||||
* The URL of your authorization server.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* issuer: "https://auth.myserver.com"
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
issuer: string;
|
||||
/**
|
||||
* A list of OIDC scopes that you want to request.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* scopes: ["openid", "profile", "email"]
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
scopes?: string[];
|
||||
/**
|
||||
* Any additional parameters that you want to pass to the authorization endpoint.
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* query: {
|
||||
* prompt: "consent"
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
query?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type OidcWrappedConfig = Omit<OidcConfig, 'issuer' | 'name'>;
|
||||
|
||||
interface ProviderState {
|
||||
state: string;
|
||||
nonce: string;
|
||||
redirect: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface IdTokenResponse {
|
||||
idToken: string;
|
||||
claims: Record<string, any>;
|
||||
raw: Record<string, any>;
|
||||
}
|
||||
|
||||
export function OidcProvider(config: OidcConfig): Provider<{ id: JWTPayload; clientID: string }> {
|
||||
const query = config.query || {};
|
||||
const scopes = config.scopes || [];
|
||||
|
||||
const wk = lazy(() =>
|
||||
fetch(config.issuer + '/.well-known/openid-configuration').then(async (r) => {
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
return r.json() as Promise<WellKnown>;
|
||||
})
|
||||
);
|
||||
|
||||
const jwks = lazy(() =>
|
||||
wk()
|
||||
.then((r) => r.jwks_uri)
|
||||
.then(async (uri) => {
|
||||
const r = await fetch(uri);
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
return createLocalJWKSet((await r.json()) as JSONWebKeySet);
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
type: config.type || 'oidc',
|
||||
init(routes, ctx) {
|
||||
routes.get('/authorize', async (c) => {
|
||||
const provider: ProviderState = {
|
||||
state: crypto.randomUUID(),
|
||||
nonce: crypto.randomUUID(),
|
||||
redirect: getRelativeUrl(c, './callback')
|
||||
};
|
||||
await ctx.set(c, 'provider', 60 * 10, provider);
|
||||
const authorization = new URL(await wk().then((r) => r.authorization_endpoint));
|
||||
authorization.searchParams.set('client_id', config.clientID);
|
||||
authorization.searchParams.set('response_type', 'id_token');
|
||||
authorization.searchParams.set('response_mode', 'form_post');
|
||||
authorization.searchParams.set('state', provider.state);
|
||||
authorization.searchParams.set('nonce', provider.nonce);
|
||||
authorization.searchParams.set('redirect_uri', provider.redirect);
|
||||
authorization.searchParams.set('scope', ['openid', ...scopes].join(' '));
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
authorization.searchParams.set(key, value);
|
||||
}
|
||||
return c.redirect(authorization.toString());
|
||||
});
|
||||
|
||||
routes.post('/callback', async (c) => {
|
||||
const provider = await ctx.get<ProviderState>(c, 'provider');
|
||||
if (!provider) return c.redirect(getRelativeUrl(c, './authorize'));
|
||||
const body = await c.req.formData();
|
||||
const error = body.get('error');
|
||||
if (error)
|
||||
throw new OauthError(
|
||||
error.toString() as any,
|
||||
body.get('error_description')?.toString() || ''
|
||||
);
|
||||
const idToken = body.get('id_token');
|
||||
if (!idToken) throw new OauthError('invalid_request', 'Missing id_token');
|
||||
const result = await jwtVerify(idToken.toString(), await jwks(), {
|
||||
audience: config.clientID
|
||||
});
|
||||
if (result.payload.nonce !== provider.nonce) {
|
||||
throw new OauthError('invalid_request', 'Invalid nonce');
|
||||
}
|
||||
return ctx.success(c, {
|
||||
id: result.payload,
|
||||
clientID: config.clientID
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user