diff --git a/README.md b/README.md index e0d0cb4c..bcfe4c7e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ const tokens = await github.validateAuthorizationCode(code); ## Supported providers +- Amazon Cognito - Apple - Atlassian - Auth0 diff --git a/docs/malta.config.json b/docs/malta.config.json index 7290f276..12a2ab7b 100644 --- a/docs/malta.config.json +++ b/docs/malta.config.json @@ -15,6 +15,7 @@ { "title": "Providers", "pages": [ + ["Amazon Cognito", "/providers/amazon-cognito"], ["Apple", "/providers/apple"], ["Atlassian", "/providers/atlassian"], ["Auth0", "/providers/auth0"], diff --git a/docs/pages/providers/amazon-cognito.md b/docs/pages/providers/amazon-cognito.md new file mode 100644 index 00000000..51297ae0 --- /dev/null +++ b/docs/pages/providers/amazon-cognito.md @@ -0,0 +1,42 @@ +--- +title: "Amazon Cognito" +--- + +# Amazon Cognito + +Implements OpenID Connect. + +For usage, see [OAuth 2.0 provider with PKCE](/guides/oauth2-pkce). + +```ts +import { AmazonCognito } from "arctic"; + +const userPoolDomain = "https://example.auth.region.amazoncognito.com"; +const amazonCognito = new AmazonCognito(userPoolDomain, clientId, clientSecret, redirectURI); +``` + +```ts +const url: URL = await amazonCognito.createAuthorizationURL(state, codeVerifier, { + // optional + scopes // "openid" always included +}); +const tokens: AmazonCognitoTokens = await amazonCognito.validateAuthorizationCode( + code, + codeVerifier +); +const tokens: AmazonCognitoRefreshedTokens = await amazonCognito.refreshAccessToken(refreshToken); +``` + +## Get user profile + +Parse the ID token or use the `userinfo` endpoint. See [sample ID token claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims). + +```ts +const tokens = await amazonCognito.validateAuthorizationCode(code, codeVerifier); +const response = await fetch(userPoolDomain + "/oauth/userInfo", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } +}); +const user = await response.json(); +``` diff --git a/src/index.ts b/src/index.ts index d32746bd..6e4851fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +export { AmazonCognito } from "./providers/amazon-cognito.js"; export { Apple } from "./providers/apple.js"; export { Atlassian } from "./providers/atlassian.js"; export { Auth0 } from "./providers/auth0.js"; @@ -27,6 +28,10 @@ export { Twitch } from "./providers/twitch.js"; export { Twitter } from "./providers/twitter.js"; export { Yahoo } from "./providers/yahoo.js"; +export type { + AmazonCognitoRefreshedTokens, + AmazonCognitoTokens +} from "./providers/amazon-cognito.js"; export type { AppleCredentials, AppleRefreshedTokens, AppleTokens } from "./providers/apple.js"; export type { AtlassianTokens } from "./providers/atlassian.js"; export type { Auth0Tokens } from "./providers/auth0.js"; diff --git a/src/providers/amazon-cognito.ts b/src/providers/amazon-cognito.ts new file mode 100644 index 00000000..a91c3aaf --- /dev/null +++ b/src/providers/amazon-cognito.ts @@ -0,0 +1,91 @@ +import { TimeSpan, createDate } from "oslo"; +import { OAuth2Client } from "oslo/oauth2"; + +import type { OAuth2ProviderWithPKCE } from "../index.js"; + +export class AmazonCognito implements OAuth2ProviderWithPKCE { + private client: OAuth2Client; + private clientSecret: string; + + constructor(userPoolDomain: string, clientId: string, clientSecret: string, redirectURI: string) { + const authorizeEndpoint = userPoolDomain + "/oauth2/authorize"; + const tokenEndpoint = userPoolDomain + "/oauth2/token"; + this.client = new OAuth2Client(clientId, authorizeEndpoint, tokenEndpoint, { + redirectURI + }); + this.clientSecret = clientSecret; + } + + public async createAuthorizationURL( + state: string, + codeVerifier: string, + options?: { + scopes?: string[]; + } + ): Promise { + const scopes = options?.scopes ?? []; + return await this.client.createAuthorizationURL({ + state, + codeVerifier, + scopes: [...scopes, "openid"] + }); + } + + public async validateAuthorizationCode( + code: string, + codeVerifier: string + ): Promise { + const result = await this.client.validateAuthorizationCode( + code, + { + credentials: this.clientSecret, + codeVerifier + } + ); + const tokens: AmazonCognitoTokens = { + accessToken: result.access_token, + refreshToken: result.refresh_token, + accessTokenExpiresAt: createDate(new TimeSpan(result.expires_in, "s")), + idToken: result.id_token + }; + return tokens; + } + + public async refreshAccessToken(refreshToken: string): Promise { + const result = await this.client.refreshAccessToken(refreshToken, { + credentials: this.clientSecret + }); + const tokens: AmazonCognitoRefreshedTokens = { + accessToken: result.access_token, + accessTokenExpiresAt: createDate(new TimeSpan(result.expires_in, "s")), + idToken: result.id_token + }; + return tokens; + } +} + +interface AuthorizationCodeResponseBody { + access_token: string; + refresh_token: string; + expires_in: number; + id_token: string; +} + +interface RefreshTokenResponseBody { + access_token: string; + expires_in: number; + id_token: string; +} + +export interface AmazonCognitoTokens { + accessToken: string; + refreshToken: string; + accessTokenExpiresAt: Date; + idToken: string; +} + +export interface AmazonCognitoRefreshedTokens { + accessToken: string; + accessTokenExpiresAt: Date; + idToken: string; +}