diff --git a/.auri/$ku1jk5lf.md b/.auri/$ku1jk5lf.md new file mode 100644 index 000000000..3d2698b1e --- /dev/null +++ b/.auri/$ku1jk5lf.md @@ -0,0 +1,6 @@ +--- +package: "lucia" # package name +type: "minor" # "major", "minor", "patch" +--- + +Add `options.responseMode` params to `apple()` \ No newline at end of file diff --git a/documentation/content/oauth/basics/oauth2.md b/documentation/content/oauth/basics/oauth2.md index 916fc8b5d..02df98158 100644 --- a/documentation/content/oauth/basics/oauth2.md +++ b/documentation/content/oauth/basics/oauth2.md @@ -48,7 +48,7 @@ url.searchParams.set("response_mode", "query"); ### Validate callback -Upon authentication, the provider will redirect the user back to your application. The url includes a code, and a state if the provider supports it. If a state is used, make sure to check if the state in the query params is the same as the one stored as a cookie. +Upon authentication, the provider will redirect the user back to your application (GET request). The url includes a code, and a state if the provider supports it. If a state is used, make sure to check if the state in the query params is the same as the one stored as a cookie. Validate the code using `validateCallback()`. If the code is valid, this will return a new [`ProviderUserAuth`](/reference/oauth/interfaces#provideruserauth) among provider specific items (such as provider user data and access tokens). See [Handle users with OAuth](/oauth/basics/handle-users) for how to use it. diff --git a/documentation/content/oauth/providers/apple.md b/documentation/content/oauth/providers/apple.md index b3b89c057..2fb57f8a9 100644 --- a/documentation/content/oauth/providers/apple.md +++ b/documentation/content/oauth/providers/apple.md @@ -36,22 +36,24 @@ const apple: ( teamId: string; keyId: string; certificate: string; - scope: string[]; + scope?: string[]; + responseMode?: "query" | "form_post"; } ) => AppleProvider; ``` ##### Parameters -| name | type | description | optional | -| -------------------- | ------------------------------------------ | -------------------------------------------------------------- | :------: | -| `auth` | [`Auth`](/reference/lucia/interfaces/auth) | Lucia instance | | -| `config.clientId` | `string` | Apple service identifier | | -| `config.redirectUri` | `string` | an authorized redirect URI | | -| `config.teamId` | `string` | Apple teamId | | -| `config.keyId ` | `string` | Apple private keyId | | -| `config.certificate` | `string` | p8 certificate as string [See how](#how-to-import-certificate) | | -| `config.scope` | `string[]` | an array of scopes | ✓ | +| name | type | description | default | +| --------------------- | ------------------------------------------ | --------------------------------------------------------------------- | --------- | +| `auth` | [`Auth`](/reference/lucia/interfaces/auth) | Lucia instance | | +| `config.clientId` | `string` | Apple service identifier | | +| `config.redirectUri` | `string` | an authorized redirect URI | | +| `config.teamId` | `string` | Apple teamId | | +| `config.keyId ` | `string` | Apple private keyId | | +| `config.certificate` | `string` | p8 certificate as string [See how](#how-to-import-certificate) | | +| `config.scope` | `string[]` | an array of scopes | `[]` | +| `config.responseMode` | `"query" \| "form_post"` | OIDC response mode - **must be `"form_post"` when requesting scopes** | `"query"` | ##### Returns @@ -83,6 +85,33 @@ export const appleAuth = apple(auth, { }); ``` +## Requesting scopes + +When requesting scopes (`email` and `name`), the `options.responseMode` must be set to `"form_post"`. Unlike the default `"query"` response mode, \*\*Apple will send an `application/x-www-form-urlencoded` POST request. You can retrieve the code by parsing the search queries or the form data. + +```ts +post("/login/apple/callback", async (request) => { + const url = new URL(request.url) + const code = url.searchParams.get("code"); + if (!isValidState(request, code)) { + // ... + } + const appleUserAuth = await + // ... +}) +``` + +Apple will also include a `user` field **only in the first response**, where you can access the user's name. + +```ts +const url = new URL(request.url); +const userJSON = url.searchParams.get("user"); +if (userJSON) { + const user = JSON.parse(userJSON); + const { firstName, lastName, email } = user; +} +``` + ## Interfaces ### `AppleAuth` @@ -124,10 +153,6 @@ type AppleTokens = { type AppleUser = { email?: string; email_verified?: boolean; - name?: { - firstName: string; - lastName: string; - }; sub: string; }; ``` diff --git a/examples/tauri/github-oauth/app/index.html b/examples/tauri/github-oauth/app/index.html index 4fbf11dce..b5de1edde 100644 --- a/examples/tauri/github-oauth/app/index.html +++ b/examples/tauri/github-oauth/app/index.html @@ -17,7 +17,7 @@

Profile

- +
diff --git a/packages/oauth/src/providers/apple.ts b/packages/oauth/src/providers/apple.ts index a91a77913..8ecc08d4c 100644 --- a/packages/oauth/src/providers/apple.ts +++ b/packages/oauth/src/providers/apple.ts @@ -16,7 +16,8 @@ type Config = { teamId: string; keyId: string; certificate: string; - scope: string[]; + responseMode?: "query" | "form_post"; + scope?: string[]; }; const PROVIDER_ID = "apple"; @@ -51,14 +52,14 @@ export class AppleAuth<_Auth extends Auth = Auth> extends OAuth2ProviderAuth< scope: scopeConfig } ); - url.searchParams.set("response_mode", "query"); + url.searchParams.set("response_mode", this.config.responseMode ?? "query"); return [url, state]; }; public validateCallback = async ( code: string ): Promise> => { - const [appleTokens, userJSON] = await this.validateAuthorizationCode(code); + const appleTokens = await this.validateAuthorizationCode(code); const idTokenPayload = decodeIdToken<{ sub: string; email?: string; @@ -67,15 +68,14 @@ export class AppleAuth<_Auth extends Auth = Auth> extends OAuth2ProviderAuth< const appleUser: AppleUser = { sub: idTokenPayload.sub, email: idTokenPayload.email, - email_verified: idTokenPayload.email_verified, - name: userJSON.name + email_verified: idTokenPayload.email_verified }; return new AppleUserAuth(this.auth, appleUser, appleTokens); }; private validateAuthorizationCode = async ( code: string - ): Promise<[tokens: AppleTokens, userJSON: AppleUserJSON]> => { + ): Promise => { const clientSecret = await createSecretId({ certificate: this.config.certificate, teamId: this.config.teamId, @@ -87,7 +87,6 @@ export class AppleAuth<_Auth extends Auth = Auth> extends OAuth2ProviderAuth< refresh_token?: string; expires_in: number; id_token: string; - user: AppleUserJSON; }>(code, "https://appleid.apple.com/auth/token", { clientId: this.config.clientId, redirectUri: this.config.redirectUri, @@ -97,15 +96,12 @@ export class AppleAuth<_Auth extends Auth = Auth> extends OAuth2ProviderAuth< } }); - return [ - { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token ?? null, - accessTokenExpiresIn: tokens.expires_in, - idToken: tokens.id_token - }, - tokens.user - ]; + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token ?? null, + accessTokenExpiresIn: tokens.expires_in, + idToken: tokens.id_token + }; }; } @@ -148,13 +144,6 @@ const createSecretId = async (config: { return jwt; }; -type AppleUserJSON = { - name?: { - firstName: string; - lastName: string; - }; -}; - export type AppleTokens = { accessToken: string; refreshToken: string | null; @@ -165,9 +154,5 @@ export type AppleTokens = { export type AppleUser = { email?: string; email_verified?: boolean; - name?: { - firstName: string; - lastName: string; - }; sub: string; };