From e1777fb088f01a628710c798b2220c1e8b1471a6 Mon Sep 17 00:00:00 2001 From: rdlabo Date: Tue, 8 Sep 2026 08:39:42 +0900 Subject: [PATCH 1/2] docs: clarify social authentication trust boundaries --- packages/hono-kit/README.md | 28 ++++++- packages/hono-kit/src/firebase/social-auth.ts | 78 +++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/packages/hono-kit/README.md b/packages/hono-kit/README.md index 2e961b3..89acc13 100644 --- a/packages/hono-kit/README.md +++ b/packages/hono-kit/README.md @@ -82,10 +82,30 @@ compatibility aliases keep the same runtime identity and signatures; there is no Kit-owned helpers such as `reopenGuardedPaymentFailedSet`, `/mysql` `createContainerRuntime`, and Firebase/auth/KV/Stripe test helpers are not deprecated by this migration. -Google and Apple login endpoints can use the root exports `verifyGoogleIdentityToken`, -`verifyAppleIdentityToken`, and `hasFirebaseProviderIdentity`. Apple token exchange can additionally -use `createAppleClientSecret`. Applications keep ownership of HTTP responses, persistence, and -provider token exchange or revocation; the Kit owns only reusable token verification and signing. +### Social auth (Google / Apple) + +Root exports `verifyGoogleIdentityToken`, `verifyAppleIdentityToken`, +`hasFirebaseProviderIdentity`, and `createAppleClientSecret` cover reusable OIDC subject +verification, Firebase identity matching, and Apple `client_secret` signing. Applications keep +ownership of HTTP responses, persistence, and provider token exchange or revocation. + +`hasFirebaseProviderIdentity` expects a **verified** Firebase ID-token payload. Pass +`requireSignInProvider: true` when an endpoint requires sign-in through that provider +(subject linked **and** `firebase.sign_in_provider` matches). Leave it `false` (default) +for link/unlink checks or combined login/link endpoints that also accept sessions established +through another provider. This policy belongs to the application. + +```ts +import { hasFirebaseProviderIdentity, verifyGoogleIdentityToken } from '@rdlabo/workers-hono-kit'; + +const subject = await verifyGoogleIdentityToken(googleIdToken, GOOGLE_CLIENT_IDS); +// login: require active Google sign-in +if (!hasFirebaseProviderIdentity(userRecord, 'google.com', subject, true)) { + return c.json({ error: 'Google identity mismatch' }, 401); +} +// link / unlink: subject present is enough +// hasFirebaseProviderIdentity(userRecord, 'google.com', subject) +``` ## Documentation diff --git a/packages/hono-kit/src/firebase/social-auth.ts b/packages/hono-kit/src/firebase/social-auth.ts index 15dc12f..9e63288 100644 --- a/packages/hono-kit/src/firebase/social-auth.ts +++ b/packages/hono-kit/src/firebase/social-auth.ts @@ -2,12 +2,34 @@ import { SignJWT, createRemoteJWKSet, importPKCS8, jwtVerify } from 'jose'; import type { JWTVerifyGetKey } from 'jose'; import type { DecodedIdToken } from './firebase-verifier.js'; +/** Apple Sign In OIDC issuer (`iss`) expected by {@link verifyAppleIdentityToken}. */ export const APPLE_IDENTITY_ISSUER = 'https://appleid.apple.com'; + +/** + * Google OIDC issuers accepted by {@link verifyGoogleIdentityToken}. + * + * Google issues tokens with either the HTTPS form or the host-only form of + * `accounts.google.com`; both are listed so verification matches either claim. + */ export const GOOGLE_IDENTITY_ISSUERS = ['https://accounts.google.com', 'accounts.google.com'] as const; const appleJwks = createRemoteJWKSet(new URL(`${APPLE_IDENTITY_ISSUER}/auth/keys`)); const googleJwks = createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs')); +/** + * Verify an Apple identity token and return its `sub` (Apple user id). + * + * Checks signature against Apple's JWKS (or an injected key resolver), enforces + * {@link APPLE_IDENTITY_ISSUER}, and requires `aud` to equal the configured Services ID / + * bundle id (`audience`). + * + * @param idToken - Raw Apple identity token JWT from Sign in with Apple. + * @param audience - Expected `aud` claim (Apple Services ID or native bundle id). + * @param getKey - Optional JWKS / key resolver; defaults to Apple's remote JWKS. Inject a + * static key in tests to avoid network I/O. + * @returns The token `sub` (Apple user identifier). + * @throws If signature, issuer, audience, or expiry fail verification, or `sub` is missing. + */ export const verifyAppleIdentityToken = async ( idToken: string, audience: string, @@ -20,6 +42,21 @@ export const verifyAppleIdentityToken = async ( return payload.sub; }; +/** + * Verify a Google identity token and return its `sub` (Google user id). + * + * Checks signature against Google's OAuth2 certs (or an injected key resolver), accepts either + * issuer in {@link GOOGLE_IDENTITY_ISSUERS}, and requires `aud` to match the configured OAuth + * client id(s) (`audience`). + * + * @param idToken - Raw Google ID token JWT. + * @param audience - Expected `aud` claim: a single OAuth client id, or a list when native and + * web clients share one login endpoint. + * @param getKey - Optional JWKS / key resolver; defaults to Google's remote certs. Inject a + * static key in tests to avoid network I/O. + * @returns The token `sub` (Google user identifier). + * @throws If signature, issuer, audience, or expiry fail verification, or `sub` is missing. + */ export const verifyGoogleIdentityToken = async ( idToken: string, audience: string | readonly string[], @@ -35,6 +72,29 @@ export const verifyGoogleIdentityToken = async ( return payload.sub; }; +/** + * Return whether a **verified** Firebase ID token already carries the given provider subject. + * + * Reads `firebase.identities[providerId]` for `subject`. When `requireSignInProvider` is true, + * also requires `firebase.sign_in_provider === providerId` so the session was established with + * that provider (login), not merely that the identity is linked while signed in another way. + * + * @remarks + * `token` must already be a verified Firebase ID-token payload (for example from + * {@link FirebaseVerifier.verifyIdToken} or auth middleware). This helper does not verify the + * Firebase JWT itself. + * + * Typical policy: + * - **Login** (`requireSignInProvider: true`): subject present and active sign-in provider matches. + * - **Link / unlink / linkage checks** (default `false`): subject present in identities only. + * + * @param token - Verified Firebase ID-token payload (`DecodedIdToken`). + * @param providerId - Firebase provider id (e.g. `'google.com'`, `'apple.com'`). + * @param subject - Provider subject previously returned by {@link verifyGoogleIdentityToken} or + * {@link verifyAppleIdentityToken}. + * @param requireSignInProvider - When `true`, also require `sign_in_provider === providerId`. + * @returns `true` when the identity (and optional active provider) matches. + */ export const hasFirebaseProviderIdentity = ( token: DecodedIdToken, providerId: string, @@ -52,12 +112,30 @@ export const hasFirebaseProviderIdentity = ( ); }; +/** + * Apple developer credentials used to mint a Sign in with Apple `client_secret` JWT. + */ export interface AppleClientSecretConfig { + /** PEM-encoded PKCS#8 ES256 private key from the Apple developer key. */ privateKey: string; + /** Key id (`kid`) of the Apple developer key. */ keyId: string; + /** Apple Team ID used as the JWT `iss` claim. */ teamId: string; } +/** + * Create a short-lived Sign in with Apple `client_secret` (ES256 JWT). + * + * The JWT is issued for `clientId` as `sub`, audience {@link APPLE_IDENTITY_ISSUER}, and expires + * 120 seconds after `now`. Used for Apple's token and revoke endpoints. + * + * @param config - Apple Team ID, key id, and PKCS#8 private key. + * @param clientId - Apple Services ID or native bundle id (`sub` claim). + * @param now - Unix time in seconds for `iat` / `exp`; injectable for deterministic tests. + * Defaults to the system clock. + * @returns A compact ES256 JWT suitable as Apple's `client_secret`. + */ export const createAppleClientSecret = async ( config: AppleClientSecretConfig, clientId: string, From 08e7752ce58aba263cd09dda41443e109beaa8c3 Mon Sep 17 00:00:00 2001 From: rdlabo Date: Tue, 8 Sep 2026 08:41:54 +0900 Subject: [PATCH 2/2] docs: show verified Firebase payload in social auth example --- packages/hono-kit/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/hono-kit/README.md b/packages/hono-kit/README.md index 89acc13..40dfe11 100644 --- a/packages/hono-kit/README.md +++ b/packages/hono-kit/README.md @@ -98,13 +98,15 @@ through another provider. This policy belongs to the application. ```ts import { hasFirebaseProviderIdentity, verifyGoogleIdentityToken } from '@rdlabo/workers-hono-kit'; +// firebaseVerifier is the application's configured FirebaseVerifier. +const verifiedFirebaseToken = await firebaseVerifier.verifyIdToken(firebaseIdToken); const subject = await verifyGoogleIdentityToken(googleIdToken, GOOGLE_CLIENT_IDS); // login: require active Google sign-in -if (!hasFirebaseProviderIdentity(userRecord, 'google.com', subject, true)) { +if (!hasFirebaseProviderIdentity(verifiedFirebaseToken, 'google.com', subject, true)) { return c.json({ error: 'Google identity mismatch' }, 401); } // link / unlink: subject present is enough -// hasFirebaseProviderIdentity(userRecord, 'google.com', subject) +// hasFirebaseProviderIdentity(verifiedFirebaseToken, 'google.com', subject) ``` ## Documentation