mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-20 20:26:55 -05:00
feat(cognito): add amazon cognito provider (#4229)
This commit is contained in:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user