feat(cognito): add amazon cognito provider (#4229)

This commit is contained in:
Shobhit Patra
2025-08-29 11:07:03 -07:00
committed by GitHub
parent e7216ff530
commit 02752a4dbc
4 changed files with 365 additions and 0 deletions
+19
View File
@@ -510,6 +510,25 @@ export const contents: Content[] = [
</svg>
),
},
{
title: "Cognito",
href: "/docs/authentication/cognito",
isNew: true,
icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 48 48"
>
<path
fill="currentColor"
d="M24 4L6 14v20l18 10 18-10V14L24 4zm0 4.62l13.6 7.86v15.04L24 39.38 10.4 31.52V16.48L24 8.62z"
/>
<path fill="currentColor" d="M22 14h4v20h-4zM14 22h20v4H14z" />
</svg>
),
},
{
title: "Discord",
href: "/docs/authentication/discord",
@@ -0,0 +1,78 @@
---
title: Cognito
description: Amazon Cognito provider setup and usage.
---
<Steps>
<Step>
### Get your Cognito Credentials
To integrate with Cognito, you need to set up a **User Pool** and an **App client** in the [Amazon Cognito Console](https://console.aws.amazon.com/cognito/).
Follow these steps:
1. Go to the **Cognito Console** and create a **User Pool**.
2. Under **App clients**, create a new **App client** (note the Client ID and Client Secret if enabled).
3. Go to **Domain** and set a Cognito Hosted UI domain (e.g., `your-app.auth.us-east-1.amazoncognito.com`).
4. In **App client settings**, enable:
- Allowed OAuth flows: `Authorization code grant`
- Allowed OAuth scopes: `openid`, `profile`, `email`
5. Add your callback URL (e.g., `http://localhost:3000/api/auth/callback/cognito`).
<Callout type="info">
- **User Pool is required** for Cognito authentication.
- Make sure the callback URL matches exactly what you configure in Cognito.
</Callout>
</Step>
<Step>
### Configure the provider
Import the Cognito provider and pass it to the `providers` option of your `auth` instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProvider: {
cognito({
clientId: process.env.COGNITO_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.COGNITO_CLIENT_SECRET as string, // [!code highlight]
domain: process.env.COGNITO_DOMAIN as string, // e.g. "your-app.auth.us-east-1.amazoncognito.com" [!code highlight]
region: process.env.COGNITO_REGION as string, // e.g. "us-east-1" [!code highlight]
userPoolId: process.env.COGNITO_USERPOOL_ID as string, // [!code highlight]
}),
},
})
```
</Step>
<Step>
### Sign In with Cognito
To sign in with Cognito, use the `signIn.social` function from the client.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "cognito"
})
}
```
### Additional Options:
- `scope`: Additional OAuth2 scopes to request (combined with default permissions).
- Default: `"openid" "profile" "email"`
- Common Cognito scopes:
- `openid`: Required for OpenID Connect authentication
- `profile`: Access to basic profile info
- `email`: Access to user’s email
- `phone`: Access to user’s phone number
- `aws.cognito.signin.user.admin`: Grants access to Cognito-specific APIs
- Note: You must configure the scopes in your Cognito App Client settings. [available scopes](https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html#token-endpoint-userinfo)
- `getUserInfo`: Custom function to retrieve user information from the Cognito UserInfo endpoint.
<Callout type="info">
For more information about Amazon Cognito's scopes and API capabilities, refer to the [official documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html?utm_source).
</Callout>
</Step>
</Steps>
@@ -0,0 +1,265 @@
import { betterFetch } from "@better-fetch/fetch";
import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
import { BetterAuthError } from "../error";
import type { OAuthProvider, ProviderOptions } from "../oauth2";
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
import { logger } from "../utils/logger";
import { refreshAccessToken } from "../oauth2/refresh-access-token";
import { APIError } from "better-call";
export interface CognitoProfile {
sub: string;
email: string;
email_verified: boolean;
name: string;
given_name?: string;
family_name?: string;
picture?: string;
username?: string;
locale?: string;
phone_number?: string;
phone_number_verified?: boolean;
aud: string;
iss: string;
exp: number;
iat: number;
// Custom attributes from Cognito can be added here
[key: string]: any;
}
export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
/**
* The Cognito domain (e.g., "your-app.auth.us-east-1.amazoncognito.com")
*/
domain: string;
/**
* AWS region where User Pool is hosted (e.g., "us-east-1")
*/
region: string;
userPoolId: string;
requireClientSecret?: boolean;
}
export const cognito = (options: CognitoOptions) => {
if (!options.domain || !options.region || !options.userPoolId) {
logger.error(
"Domain, region and userPoolId are required for Amazon Cognito. Make sure to provide them in the options.",
);
throw new BetterAuthError("DOMAIN_AND_REGION_REQUIRED");
}
const cleanDomain = options.domain.replace(/^https?:\/\//, "");
const authorizationEndpoint = `https://${cleanDomain}/oauth2/authorize`;
const tokenEndpoint = `https://${cleanDomain}/oauth2/token`;
const userInfoEndpoint = `https://${cleanDomain}/oauth2/userinfo`;
return {
id: "cognito",
name: "Cognito",
async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
if (!options.clientId) {
logger.error(
"ClientId is required for Amazon Cognito. Make sure to provide them in the options.",
);
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
}
if (options.requireClientSecret && !options.clientSecret) {
logger.error(
"Client Secret is required when requireClientSecret is true. Make sure to provide it in the options.",
);
throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
}
const _scopes = options.disableDefaultScope
? []
: ["openid", "profile", "email"];
options.scope && _scopes.push(...options.scope);
scopes && _scopes.push(...scopes);
const url = await createAuthorizationURL({
id: "cognito",
options: {
...options,
},
authorizationEndpoint,
scopes: _scopes,
state,
codeVerifier,
redirectURI,
prompt: options.prompt,
});
return url;
},
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
return validateAuthorizationCode({
code,
codeVerifier,
redirectURI,
options,
tokenEndpoint,
});
},
refreshAccessToken: options.refreshAccessToken
? options.refreshAccessToken
: async (refreshToken) => {
return refreshAccessToken({
refreshToken,
options: {
clientId: options.clientId,
clientKey: options.clientKey,
clientSecret: options.clientSecret,
},
tokenEndpoint,
});
},
async verifyIdToken(token, nonce) {
if (options.disableIdTokenSignIn) {
return false;
}
if (options.verifyIdToken) {
return options.verifyIdToken(token, nonce);
}
try {
const decodedHeader = decodeProtectedHeader(token);
const { kid, alg: jwtAlg } = decodedHeader;
if (!kid || !jwtAlg) return false;
const publicKey = await getCognitoPublicKey(
kid,
options.region,
options.userPoolId,
);
const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
algorithms: [jwtAlg],
issuer: expectedIssuer,
audience: options.clientId,
maxTokenAge: "1h",
});
if (nonce && jwtClaims.nonce !== nonce) {
return false;
}
return true;
} catch (error) {
logger.error("Failed to verify ID token:", error);
return false;
}
},
async getUserInfo(token) {
if (options.getUserInfo) {
return options.getUserInfo(token);
}
if (token.idToken) {
try {
const profile = decodeJwt<CognitoProfile>(token.idToken);
if (!profile) {
return null;
}
const name =
profile.name ||
profile.given_name ||
profile.username ||
profile.email;
const enrichedProfile = {
...profile,
name,
};
const userMap = await options.mapProfileToUser?.(enrichedProfile);
return {
user: {
id: profile.sub,
name: enrichedProfile.name,
email: profile.email,
image: profile.picture,
emailVerified: profile.email_verified,
...userMap,
},
data: enrichedProfile,
};
} catch (error) {
logger.error("Failed to decode ID token:", error);
}
}
if (token.accessToken) {
try {
const { data: userInfo } = await betterFetch<CognitoProfile>(
userInfoEndpoint,
{
headers: {
Authorization: `Bearer ${token.accessToken}`,
},
},
);
if (userInfo) {
const userMap = await options.mapProfileToUser?.(userInfo);
return {
user: {
id: userInfo.sub,
name: userInfo.name || userInfo.given_name || userInfo.username,
email: userInfo.email,
image: userInfo.picture,
emailVerified: userInfo.email_verified,
...userMap,
},
data: userInfo,
};
}
} catch (error) {
logger.error("Failed to fetch user info from Cognito:", error);
}
}
return null;
},
options,
} satisfies OAuthProvider<CognitoProfile>;
};
export const getCognitoPublicKey = async (
kid: string,
region: string,
userPoolId: string,
) => {
const COGNITO_JWKS_URI = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`;
try {
const { data } = await betterFetch<{
keys: Array<{
kid: string;
alg: string;
kty: string;
use: string;
n: string;
e: string;
}>;
}>(COGNITO_JWKS_URI);
if (!data?.keys) {
throw new APIError("BAD_REQUEST", {
message: "Keys not found",
});
}
const jwk = data.keys.find((key) => key.kid === kid);
if (!jwk) {
throw new Error(`JWK with kid ${kid} not found`);
}
return await importJWK(jwk, jwk.alg);
} catch (error) {
logger.error("Failed to fetch Cognito public key:", error);
throw error;
}
};
@@ -2,6 +2,7 @@ import * as z from "zod/v4";
import type { Prettify } from "../types/helper";
import { apple } from "./apple";
import { atlassian } from "./atlassian";
import { cognito } from "./cognito";
import { discord } from "./discord";
import { facebook } from "./facebook";
import { figma } from "./figma";
@@ -33,6 +34,7 @@ import { paypal } from "./paypal";
export const socialProviders = {
apple,
atlassian,
cognito,
discord,
facebook,
figma,
@@ -83,6 +85,7 @@ export type SocialProviders = {
export * from "./apple";
export * from "./atlassian";
export * from "./cognito";
export * from "./discord";
export * from "./dropbox";
export * from "./facebook";