From 6a06ebeee76abe0eb2c12f663ec4b0710609eb88 Mon Sep 17 00:00:00 2001 From: Jake Palmer Date: Sun, 5 Jan 2025 00:59:07 -0600 Subject: [PATCH 1/4] feat: add Apple OAuth support with response_mode parameter --- src/authorize-endpoint.ts | 9 ++++++++- src/types.ts | 10 ++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/authorize-endpoint.ts b/src/authorize-endpoint.ts index 168ff0a..632ee8a 100644 --- a/src/authorize-endpoint.ts +++ b/src/authorize-endpoint.ts @@ -20,7 +20,14 @@ export const createAuthorizeEndpoint = ( const responseType = "code"; const accessType = "offline"; - const authorizeUrl = `${pluginOptions.providerAuthorizationUrl}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=${responseType}&access_type=${accessType}${prompt}`; + + + // Add response_mode if specified + const responseMode = pluginOptions.responseMode + ? `&response_mode=${pluginOptions.responseMode}` + : '' + + const authorizeUrl = `${pluginOptions.providerAuthorizationUrl}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=${responseType}&access_type=${accessType}${prompt}${responseMode}`; return Response.redirect(authorizeUrl); }, diff --git a/src/types.ts b/src/types.ts index 1e1eeef..6047c0e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,6 +27,16 @@ export interface PluginTypes { */ serverURL: string; + /** + * Response mode for the OAuth provider. + * Required for Apple OAuth when requesting name or email scope. + * Common values: + * - 'form_post': Response parameters encoded in POST body (required for Apple with name/email scope) + * - 'query': Response parameters encoded in URL query string + * @default undefined + */ + responseMode?: string + /** * Slug of the collection where user information will be stored * @default "users" From 695de7fab4d0a9f134f376a27e18f8a262c2a008 Mon Sep 17 00:00:00 2001 From: Jake Palmer Date: Sun, 5 Jan 2025 01:50:31 -0600 Subject: [PATCH 2/4] feat: add form_post response mode support for Apple OAuth - Add responseMode parameter to support different OAuth response types - Update callback endpoint to handle both GET and POST methods - Maintain backward compatibility with existing OAuth implementations --- src/callback-endpoint.ts | 22 +++++++++++++++------- src/types.ts | 22 +++++++++++++++++++--- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/callback-endpoint.ts b/src/callback-endpoint.ts index 1362fe9..315a00b 100644 --- a/src/callback-endpoint.ts +++ b/src/callback-endpoint.ts @@ -12,15 +12,23 @@ import { PluginTypes } from "./types"; export const createCallbackEndpoint = ( pluginOptions: PluginTypes, ): Endpoint => ({ - method: "get", - path: pluginOptions.callbackPath || "/oauth/callback", + // Support both GET (default OAuth2) and POST (required for Apple OAuth with form_post) + // - GET: Used by most OAuth providers (Google, GitHub, etc.) + // - POST: Required by Apple when requesting name/email scopes with response_mode=form_post + method: ['get', 'post'], + path: pluginOptions.callbackPath || '/oauth/callback', handler: async (req) => { try { - const { code } = req.query; - if (typeof code !== "string") - throw new Error( - `Code not in query string: ${JSON.stringify(req.query)}`, - ); + // Handle authorization code from both GET query params and POST body + // This enables support for Apple's form_post response mode while maintaining + // compatibility with traditional OAuth2 GET responses + const code = req.method === 'POST' ? req.body?.code : req.query?.code + // Improved error handling to clearly indicate whether we're missing the code + // from POST body (Apple OAuth) or GET query parameters (standard OAuth) + if (typeof code !== 'string') + throw new Error( + `Code not found in ${req.method === 'POST' ? 'body' : 'query'}: ${JSON.stringify(req.method === 'POST' ? req.body : req.query)}`, + ) // ///////////////////////////////////// // shorthands diff --git a/src/types.ts b/src/types.ts index 6047c0e..8dcdeb2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,16 +27,32 @@ export interface PluginTypes { */ serverURL: string; - /** + /** * Response mode for the OAuth provider. - * Required for Apple OAuth when requesting name or email scope. + * Specifies how the authorization response should be returned. + * + * Required for Apple OAuth when requesting name or email scopes. + * Apple requires 'form_post' when requesting these scopes to ensure + * secure transmission of user data. + * * Common values: * - 'form_post': Response parameters encoded in POST body (required for Apple with name/email scope) - * - 'query': Response parameters encoded in URL query string + * - 'query': Response parameters encoded in URL query string (default for most providers) + * - 'fragment': Response parameters encoded in URL fragment + * + * Example for Apple OAuth: + * ```typescript + * { + * responseMode: 'form_post', + * scopes: ['name', 'email'] + * } + * ``` + * * @default undefined */ responseMode?: string + /** * Slug of the collection where user information will be stored * @default "users" From 1a966a6d46831a457d6338326af9e11d574e8368 Mon Sep 17 00:00:00 2001 From: Jake Palmer Date: Sun, 5 Jan 2025 02:01:46 -0600 Subject: [PATCH 3/4] feat: add form_post response mode support for Apple OAuth --- package.json | 5 +++-- src/callback-endpoint.ts | 16 ++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 576e8f3..7461c27 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "module", "homepage:": "https://payloadcms.com", "repository": "https://github.com/WilsonLe/payload-oauth2", - "description": "OAuth2 plugin for Payload CMS", + "description": "OAuth2 plugin for Payload CMS with Apple Sign In support", "main": "dist/index.js", "types": "dist/index.d.ts", "keywords": [ @@ -14,7 +14,8 @@ "typescript", "react", "oauth2", - "payload-plugin" + "payload-plugin", + "apple-sign-in" ], "files": [ "dist" diff --git a/src/callback-endpoint.ts b/src/callback-endpoint.ts index 315a00b..6ed34b3 100644 --- a/src/callback-endpoint.ts +++ b/src/callback-endpoint.ts @@ -15,19 +15,23 @@ export const createCallbackEndpoint = ( // Support both GET (default OAuth2) and POST (required for Apple OAuth with form_post) // - GET: Used by most OAuth providers (Google, GitHub, etc.) // - POST: Required by Apple when requesting name/email scopes with response_mode=form_post - method: ['get', 'post'], + method: 'post' as const, path: pluginOptions.callbackPath || '/oauth/callback', handler: async (req) => { try { - // Handle authorization code from both GET query params and POST body - // This enables support for Apple's form_post response mode while maintaining - // compatibility with traditional OAuth2 GET responses - const code = req.method === 'POST' ? req.body?.code : req.query?.code + // Handle authorization code from both GET query params and POST body + // This enables support for Apple's form_post response mode while maintaining + // compatibility with traditional OAuth2 GET responses + const code = req.method === "POST" + ? (req.body)?.code // Type assertion for body + : (req.query)?.code // Type assertion for query // Improved error handling to clearly indicate whether we're missing the code // from POST body (Apple OAuth) or GET query parameters (standard OAuth) if (typeof code !== 'string') throw new Error( - `Code not found in ${req.method === 'POST' ? 'body' : 'query'}: ${JSON.stringify(req.method === 'POST' ? req.body : req.query)}`, + `Code not found in ${req.method === 'POST' ? 'body' : 'query'}: ${JSON.stringify( + req.method === 'POST' ? req.body : req.query + )}`, ) // ///////////////////////////////////// From 8a31631088c4d51411c72df33586f4194e2cdbed Mon Sep 17 00:00:00 2001 From: Jake Palmer Date: Sun, 5 Jan 2025 04:18:06 -0600 Subject: [PATCH 4/4] feat: add Apple OAuth support with form_post response mode --- dev/.env.example | 16 ++++ examples/apple.ts | 100 +++++++++++++++++++++++ src/authorize-endpoint.ts | 17 ++-- src/callback-endpoint.ts | 161 +++++++++++++++++++++++--------------- src/types.ts | 58 +++++++------- 5 files changed, 252 insertions(+), 100 deletions(-) create mode 100644 examples/apple.ts diff --git a/dev/.env.example b/dev/.env.example index 9caaa01..8c74a32 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -27,3 +27,19 @@ ZITADEL_CLIENT_ID=298062958548846024 # optional: google oauth2 client secret, not activated if not set ZITADEL_CLIENT_SECRET=q2isqvfws3kg0af4ksiek3jigwre5utfoxgwkjj7mkbsmpgdyv1abyq6jcgpakch + +################################################################################ +# Apple OAuth Config +################################################################################ +# Optional: Apple OAuth2 Client ID (Services ID), not activated if not set +# APPLE_CLIENT_ID=com.your.app.id + +# Optional: Apple OAuth2 Client Secret (Generated from Apple Developer Portal) +# APPLE_CLIENT_SECRET=your-generated-secret + +# Note: For Apple OAuth, you need to: +# 1. Create an App ID in Apple Developer Portal +# 2. Create a Services ID +# 3. Configure domain association +# 4. Generate a Client Secret +# See: https://developer.apple.com/sign-in-with-apple/get-started/ diff --git a/examples/apple.ts b/examples/apple.ts new file mode 100644 index 0000000..f440c0f --- /dev/null +++ b/examples/apple.ts @@ -0,0 +1,100 @@ +import { PayloadRequest } from "payload"; +import { OAuth2Plugin } from "../src/index"; + +//////////////////////////////////////////////////////////////////////////////// +// Apple OAuth +//////////////////////////////////////////////////////////////////////////////// +export const appleOAuth = OAuth2Plugin({ + enabled: + typeof process.env.APPLE_CLIENT_ID === "string" && + typeof process.env.APPLE_CLIENT_SECRET === "string", + strategyName: "apple", + useEmailAsIdentity: true, + serverURL: process.env.NEXT_PUBLIC_URL || "http://localhost:3000", + clientId: process.env.APPLE_CLIENT_ID || "", + clientSecret: process.env.APPLE_CLIENT_SECRET || "", + authorizePath: "/oauth/apple", + callbackPath: "/oauth/apple/callback", + authCollection: "users", + tokenEndpoint: "https://appleid.apple.com/auth/token", + scopes: ["name", "email"], + providerAuthorizationUrl: "https://appleid.apple.com/auth/authorize", + // Required for Apple OAuth when requesting name or email scopes + responseMode: "form_post", + getUserInfo: async (accessToken: string, req: PayloadRequest) => { + try { + // For Apple, the ID token is a JWT that contains user info + const tokenParts = accessToken.split("."); + if (tokenParts.length !== 3) { + throw new Error("Invalid ID token format"); + } + + // Decode the base64 payload + const payload = JSON.parse(Buffer.from(tokenParts[1], "base64").toString()); + + if (!payload.email) { + throw new Error("No email found in payload"); + } + + return { + email: payload.email, + sub: payload.sub, + // Apple provides name only on first login + firstName: payload.given_name || "", + lastName: payload.family_name || "", + }; + } catch (error) { + req.payload.logger.error("Error parsing Apple token:", error); + throw error; + } + }, + getToken: async (code: string, req: PayloadRequest) => { + try { + const redirectUri = `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}/api/users/oauth/apple/callback`; + + // Make the token exchange request + const params = new URLSearchParams({ + client_id: process.env.APPLE_CLIENT_ID || "", + client_secret: process.env.APPLE_CLIENT_SECRET || "", + code: code, + grant_type: "authorization_code", + redirect_uri: redirectUri, + }); + + const response = await fetch("https://appleid.apple.com/auth/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params.toString(), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + const tokenResponse = await response.json(); + + // Return the id_token which contains the user info + return tokenResponse.id_token; + } catch (error) { + req.payload.logger.error("Error in getToken:", error); + throw error; + } + }, + successRedirect: (req) => { + // Check user roles to determine redirect + const user = req.user; + if (user && Array.isArray(user.roles)) { + if (user.roles.includes("admin")) { + return "/admin"; + } + } + return "/"; // Default redirect for customers + }, + failureRedirect: (req, err) => { + req.payload.logger.error(err); + return "/login?error=apple-auth-failed"; + }, +}); diff --git a/src/authorize-endpoint.ts b/src/authorize-endpoint.ts index 632ee8a..fc722b0 100644 --- a/src/authorize-endpoint.ts +++ b/src/authorize-endpoint.ts @@ -1,5 +1,5 @@ -import { Endpoint } from "payload"; -import { PluginTypes } from "./types"; +import type { Endpoint } from "payload"; +import type { PluginTypes } from "./types"; export const createAuthorizeEndpoint = ( pluginOptions: PluginTypes, @@ -21,13 +21,14 @@ export const createAuthorizeEndpoint = ( const responseType = "code"; const accessType = "offline"; + // Add response_mode if specified (required for Apple OAuth with name/email scopes) + const responseMode = pluginOptions.responseMode + ? `&response_mode=${pluginOptions.responseMode}` + : ""; - // Add response_mode if specified - const responseMode = pluginOptions.responseMode - ? `&response_mode=${pluginOptions.responseMode}` - : '' - - const authorizeUrl = `${pluginOptions.providerAuthorizationUrl}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=${responseType}&access_type=${accessType}${prompt}${responseMode}`; + const authorizeUrl = `${ + pluginOptions.providerAuthorizationUrl + }?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=${responseType}&access_type=${accessType}${prompt}${responseMode}`; return Response.redirect(authorizeUrl); }, diff --git a/src/callback-endpoint.ts b/src/callback-endpoint.ts index 6ed34b3..3f5c488 100644 --- a/src/callback-endpoint.ts +++ b/src/callback-endpoint.ts @@ -1,38 +1,51 @@ -import crypto from "crypto"; +import crypto from "node:crypto"; import { SignJWT } from "jose"; -import { +import type { CollectionSlug, Endpoint, - generatePayloadCookie, - getFieldsToSign, + PayloadHandler, + PayloadRequest, + RequestContext, + User, } from "payload"; +import { generatePayloadCookie, getFieldsToSign } from "payload"; import { defaultGetToken } from "./default-get-token"; -import { PluginTypes } from "./types"; +import type { PluginTypes } from "./types"; export const createCallbackEndpoint = ( pluginOptions: PluginTypes, -): Endpoint => ({ - // Support both GET (default OAuth2) and POST (required for Apple OAuth with form_post) - // - GET: Used by most OAuth providers (Google, GitHub, etc.) - // - POST: Required by Apple when requesting name/email scopes with response_mode=form_post - method: 'post' as const, - path: pluginOptions.callbackPath || '/oauth/callback', - handler: async (req) => { +): Endpoint => { + const handler: PayloadHandler = async (req: PayloadRequest) => { try { // Handle authorization code from both GET query params and POST body // This enables support for Apple's form_post response mode while maintaining // compatibility with traditional OAuth2 GET responses - const code = req.method === "POST" - ? (req.body)?.code // Type assertion for body - : (req.query)?.code // Type assertion for query - // Improved error handling to clearly indicate whether we're missing the code - // from POST body (Apple OAuth) or GET query parameters (standard OAuth) - if (typeof code !== 'string') - throw new Error( - `Code not found in ${req.method === 'POST' ? 'body' : 'query'}: ${JSON.stringify( - req.method === 'POST' ? req.body : req.query - )}`, - ) + let code: string | undefined; + + if (req.method === "POST") { + // Handle form data from POST request (used by Apple OAuth) + const contentType = req.headers.get("content-type"); + if (contentType?.includes("application/x-www-form-urlencoded")) { + const text = await (req as unknown as Request).text(); + const formData = new URLSearchParams(text); + code = formData.get("code") || undefined; + } + } else if (req.method === "GET") { + // Handle query parameters (used by Google OAuth) + code = typeof req.query === "object" && req.query ? (req.query as { code?: string }).code : undefined; + } + + // Improved error handling to clearly indicate whether we're missing the code + // from POST body (Apple OAuth) or GET query parameters (standard OAuth) + if (typeof code !== "string") { + throw new Error( + `Code not found in ${req.method === "POST" ? "body" : "query"}: ${ + req.method === "POST" + ? "form-data" + : JSON.stringify(req.query) + }`, + ); + } // ///////////////////////////////////// // shorthands @@ -49,18 +62,17 @@ export const createCallbackEndpoint = ( // ///////////////////////////////////// // beforeOperation - Collection // ///////////////////////////////////// - // Not implemented + // Not implemented - reserved for future use // ///////////////////////////////////// - // obtain access token + // obtain access token or id_token // ///////////////////////////////////// - - let access_token: string; + let token: string; if (pluginOptions.getToken) { - access_token = await pluginOptions.getToken(code, req); + token = await pluginOptions.getToken(code, req); } else { - access_token = await defaultGetToken( + token = await defaultGetToken( pluginOptions.tokenEndpoint, pluginOptions.clientId, pluginOptions.clientSecret, @@ -69,19 +81,21 @@ export const createCallbackEndpoint = ( ); } - if (typeof access_token !== "string") - throw new Error(`No access token: ${access_token}`); + if (typeof token !== "string") { + throw new Error(`Invalid token response: ${token}`); + } // ///////////////////////////////////// // get user info // ///////////////////////////////////// - const userInfo = await pluginOptions.getUserInfo(access_token, req); + const userInfo = await pluginOptions.getUserInfo(token, req); // ///////////////////////////////////// // ensure user exists // ///////////////////////////////////// - let existingUser: any; + let existingUser: { docs: Array> }; if (useEmailAsIdentity) { + // Use email as the unique identifier existingUser = await req.payload.find({ req, collection: authCollection, @@ -90,6 +104,7 @@ export const createCallbackEndpoint = ( limit: 1, }); } else { + // Use provider's sub field as the unique identifier existingUser = await req.payload.find({ req, collection: authCollection, @@ -99,43 +114,53 @@ export const createCallbackEndpoint = ( }); } - let user: any; - if (existingUser.docs.length === 0) { - user = await req.payload.create({ + let user = existingUser.docs[0] as User; + if (!user) { + // Create new user if they don't exist + const result = await req.payload.create({ req, collection: authCollection, data: { ...userInfo, - // Stuff breaks when password is missing + collection: authCollection, + // Generate secure random password for OAuth users password: crypto.randomBytes(32).toString("hex"), }, showHiddenFields: true, }); + user = result as User; } else { - user = await req.payload.update({ + // Update existing user with latest info from provider + const result = await req.payload.update({ req, collection: authCollection, - id: existingUser.docs[0].id, - data: userInfo, + id: user.id, + data: { + ...userInfo, + collection: authCollection, + }, showHiddenFields: true, }); + user = result as User; } // ///////////////////////////////////// // beforeLogin - Collection // ///////////////////////////////////// - await collectionConfig.hooks.beforeLogin.reduce( async (priorHook, hook) => { await priorHook; - user = - (await hook({ - collection: collectionConfig, - context: req.context, - req, - user, - })) || user; + const hookResult = await hook({ + collection: collectionConfig, + context: req.context || {} as RequestContext, + req, + user, + }); + + if (hookResult) { + user = hookResult as User; + } }, Promise.resolve(), ); @@ -145,11 +170,11 @@ export const createCallbackEndpoint = ( // ///////////////////////////////////// const fieldsToSign = getFieldsToSign({ collectionConfig, - email: user.email, + email: user.email || "", user, }); - const token = await new SignJWT(fieldsToSign) + const jwtToken = await new SignJWT(fieldsToSign) .setProtectedHeader({ alg: "HS256" }) .setExpirationTime(`${collectionConfig.auth.tokenExpiration} secs`) .sign(new TextEncoder().encode(req.payload.secret)); @@ -158,19 +183,21 @@ export const createCallbackEndpoint = ( // ///////////////////////////////////// // afterLogin - Collection // ///////////////////////////////////// - await collectionConfig.hooks.afterLogin.reduce( async (priorHook, hook) => { await priorHook; - user = - (await hook({ - collection: collectionConfig, - context: req.context, - req, - token, - user, - })) || user; + const hookResult = await hook({ + collection: collectionConfig, + context: req.context || {} as RequestContext, + req, + token: jwtToken, + user, + }); + + if (hookResult) { + user = hookResult as User; + } }, Promise.resolve(), ); @@ -178,7 +205,7 @@ export const createCallbackEndpoint = ( // ///////////////////////////////////// // afterRead - Fields // ///////////////////////////////////// - // Not implemented + // Not implemented - reserved for future use // ///////////////////////////////////// // generate and set cookie @@ -186,7 +213,7 @@ export const createCallbackEndpoint = ( const cookie = generatePayloadCookie({ collectionAuthConfig: collectionConfig.auth, cookiePrefix: payloadConfig.cookiePrefix, - token, + token: jwtToken, }); // ///////////////////////////////////// @@ -211,5 +238,13 @@ export const createCallbackEndpoint = ( status: 302, }); } - }, -}); + }; + + return { + // We use GET as the primary method since that's what most OAuth providers use + // The handler itself will accept both GET and POST internally + method: "get", + path: pluginOptions.callbackPath || "/oauth/callback", + handler, + }; +}; diff --git a/src/types.ts b/src/types.ts index 8dcdeb2..4921ba2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { PayloadRequest } from "payload"; +import type { PayloadRequest } from "payload"; export interface PluginTypes { /** @@ -27,31 +27,22 @@ export interface PluginTypes { */ serverURL: string; - /** - * Response mode for the OAuth provider. - * Specifies how the authorization response should be returned. - * - * Required for Apple OAuth when requesting name or email scopes. - * Apple requires 'form_post' when requesting these scopes to ensure - * secure transmission of user data. - * - * Common values: - * - 'form_post': Response parameters encoded in POST body (required for Apple with name/email scope) - * - 'query': Response parameters encoded in URL query string (default for most providers) - * - 'fragment': Response parameters encoded in URL fragment - * - * Example for Apple OAuth: - * ```typescript - * { - * responseMode: 'form_post', - * scopes: ['name', 'email'] - * } - * ``` - * - * @default undefined - */ - responseMode?: string - + /** + * Response mode for the OAuth provider. + * Specifies how the authorization response should be returned. + * + * Required for Apple OAuth when requesting name or email scopes. + * Apple requires 'form_post' when requesting these scopes to ensure + * secure transmission of user data. + * + * Common values: + * - 'form_post': Response parameters encoded in POST body (required for Apple with name/email scope) + * - 'query': Response parameters encoded in URL query string (default for most providers) + * - 'fragment': Response parameters encoded in URL fragment + * + * @default undefined + */ + responseMode?: string; /** * Slug of the collection where user information will be stored @@ -80,6 +71,7 @@ export interface PluginTypes { * URL to the token endpoint. * The following are token endpoints for popular OAuth providers: * - Google: https://oauth2.googleapis.com/token + * - Apple: https://appleid.apple.com/auth/token */ tokenEndpoint: string; @@ -88,6 +80,7 @@ export interface PluginTypes { * Must not have trailing slash. * The following are authorization endpoints for popular OAuth providers: * - Google: https://accounts.google.com/o/oauth2/v2/auth + * - Apple: https://appleid.apple.com/auth/authorize */ providerAuthorizationUrl: string; @@ -95,10 +88,14 @@ export interface PluginTypes { * Function to get user information from the OAuth provider. * This function should return a promise that resolves to the user * information that will be stored in database. - * @param accessToken Access token obtained from OAuth provider + * + * For providers that return user info in the ID token (like Apple), + * you can decode the JWT token here to extract user information. + * + * @param accessToken Access token or ID token obtained from OAuth provider * @param req PayloadRequest object */ - getUserInfo: (accessToken: string, req: PayloadRequest) => Promise | any; + getUserInfo: (accessToken: string, req: PayloadRequest) => Promise>; /** * Scope for the OAuth provider. @@ -107,6 +104,9 @@ export interface PluginTypes { * + https://www.googleapis.com/auth/userinfo.email * + https://www.googleapis.com/auth/userinfo.profile * + openid + * - Apple: + * + name + * + email */ scopes: string[]; @@ -148,7 +148,7 @@ export interface PluginTypes { * @param code Code obtained from the OAuth provider, used to exchange for access token * @param req PayloadRequest object */ - getToken?: (code: string, req: PayloadRequest) => string | Promise; + getToken?: (code: string, req: PayloadRequest) => Promise; /** * Redirect users after successful login.