Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions packages/hono-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,32 @@ 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';

// 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(verifiedFirebaseToken, 'google.com', subject, true)) {
return c.json({ error: 'Google identity mismatch' }, 401);
}
// link / unlink: subject present is enough
// hasFirebaseProviderIdentity(verifiedFirebaseToken, 'google.com', subject)
```

## Documentation

Expand Down
78 changes: 78 additions & 0 deletions packages/hono-kit/src/firebase/social-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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[],
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Loading