release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+102 -3
View File
@@ -1,4 +1,5 @@
import { randomToken, type OAuthProvider, type OAuthTokens } from "./index.ts";
import { verifyJwtWithJwks, type JwtClaims, type RemoteJwks } from "@wrnexus/jwt";
export interface OAuthStateRecord {
state: string;
@@ -70,6 +71,16 @@ export interface OidcDiscovery {
jwks_uri: string;
revocation_endpoint?: string;
}
function requireHttpsEndpoint(value: unknown, name: string): string {
if (typeof value !== "string") throw new Error(`OIDC discovery is missing ${name}`);
const url = new URL(value);
if (url.protocol !== "https:" || url.username || url.password || url.hash) {
throw new Error(`OIDC ${name} must be an HTTPS URL without credentials or a fragment`);
}
return value;
}
export async function discoverOidc(
issuer: string,
fetchImpl: typeof fetch = fetch,
@@ -77,9 +88,97 @@ export async function discoverOidc(
const base = issuer.replace(/\/$/, "");
const response = await fetchImpl(`${base}/.well-known/openid-configuration`);
if (!response.ok) throw new Error(`OIDC discovery failed (${response.status})`);
const value = (await response.json()) as OidcDiscovery;
if (value.issuer !== issuer && value.issuer !== base) throw new Error("OIDC issuer mismatch");
return value;
const value = (await response.json()) as Partial<OidcDiscovery>;
if (value.issuer !== base) throw new Error("OIDC issuer mismatch");
return {
issuer: base,
authorization_endpoint: requireHttpsEndpoint(
value.authorization_endpoint,
"authorization_endpoint",
),
token_endpoint: requireHttpsEndpoint(value.token_endpoint, "token_endpoint"),
jwks_uri: requireHttpsEndpoint(value.jwks_uri, "jwks_uri"),
userinfo_endpoint: value.userinfo_endpoint
? requireHttpsEndpoint(value.userinfo_endpoint, "userinfo_endpoint")
: undefined,
revocation_endpoint: value.revocation_endpoint
? requireHttpsEndpoint(value.revocation_endpoint, "revocation_endpoint")
: undefined,
};
}
export interface OidcIdTokenClaims extends JwtClaims {
sub: string;
iss: string;
aud: string | string[];
exp: number;
iat: number;
nonce?: string;
azp?: string;
at_hash?: string;
}
export interface VerifyOidcIdTokenOptions {
issuer: string;
clientId: string;
jwks: RemoteJwks;
nonce?: string;
accessToken?: string;
now?: number;
clockTolerance?: number;
maxAge?: number;
}
export function validateOidcClaims(
claims: JwtClaims,
options: Pick<VerifyOidcIdTokenOptions, "clientId" | "nonce">,
): asserts claims is OidcIdTokenClaims {
if (typeof claims.sub !== "string" || !claims.sub) throw new Error("OIDC token has no subject");
if (
typeof claims.iss !== "string" ||
(typeof claims.aud !== "string" && !Array.isArray(claims.aud)) ||
typeof claims.exp !== "number" ||
typeof claims.iat !== "number"
) {
throw new Error("OIDC token is missing required claims");
}
const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
if (audiences.length > 1 && claims.azp !== options.clientId)
throw new Error("OIDC token has invalid authorized party");
if (claims.azp !== undefined && claims.azp !== options.clientId)
throw new Error("OIDC token has invalid authorized party");
if (options.nonce !== undefined && claims.nonce !== options.nonce)
throw new Error("OIDC token has invalid nonce");
}
async function accessTokenHash(accessToken: string): Promise<string> {
const digest = new Uint8Array(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(accessToken)),
).slice(0, 16);
let binary = "";
for (const byte of digest) binary += String.fromCharCode(byte);
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
export async function verifyOidcIdToken(
token: string,
options: VerifyOidcIdTokenOptions,
): Promise<OidcIdTokenClaims> {
const issuer = options.issuer.replace(/\/$/, "");
const claims = await verifyJwtWithJwks(token, options.jwks, {
issuer,
audience: options.clientId,
now: options.now,
clockTolerance: options.clockTolerance,
maxAge: options.maxAge,
});
validateOidcClaims(claims, options);
if (options.accessToken !== undefined) {
if (typeof claims.at_hash !== "string") throw new Error("OIDC token has no access-token hash");
if ((await accessTokenHash(options.accessToken)) !== claims.at_hash)
throw new Error("OIDC token has invalid access-token hash");
}
return claims;
}
export function validateOAuthReturnTo(
+9 -1
View File
@@ -246,6 +246,14 @@ export {
createOAuthState,
refreshOAuthTokens,
discoverOidc,
validateOidcClaims,
verifyOidcIdToken,
validateOAuthReturnTo,
} from "./advanced.ts";
export type { OAuthStateRecord, OAuthStateStore, OidcDiscovery } from "./advanced.ts";
export type {
OAuthStateRecord,
OAuthStateStore,
OidcDiscovery,
OidcIdTokenClaims,
VerifyOidcIdTokenOptions,
} from "./advanced.ts";