Skip to content

Commit 28e3db4

Browse files
authored
feat: custom extract callback token (#47)
* custom extract callback token * fix build error * update docs
1 parent 466b8b9 commit 28e3db4

File tree

7 files changed

+88
-49
lines changed

7 files changed

+88
-49
lines changed

dev/.env.example

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,21 @@ NEXT_PUBLIC_URL=http://localhost:3000
2222
################################################################################
2323
# zitadel oauth config
2424
################################################################################
25-
# optional: google oauth2 client id, not activated if not set
25+
# optional: zitadel oauth2 client id, not activated if not set
2626
# ZITADEL_CLIENT_ID=
2727

28-
# optional: google oauth2 client secret, not activated if not set
28+
# optional: zitadel oauth2 client secret, not activated if not set
2929
# ZITADEL_CLIENT_SECRET=
3030

31+
# optional: zitadel oauth2 token endpoint, not activated if not set
32+
# ZITADEL_TOKEN_ENDPOINT=
33+
34+
# optional: zitadel oauth2 authorization url, not activated if not set
35+
# ZITADEL_AUTHORIZATION_URL=
36+
37+
# optional: zitadel oauth2 userinfo endpoint, not activated if not set
38+
# ZITADEL_USERINFO_ENDPOINT=
39+
3140
################################################################################
3241
# Microsoft Entra ID OAuth 2.0 config
3342
################################################################################
@@ -43,17 +52,6 @@ NEXT_PUBLIC_URL=http://localhost:3000
4352
# optional: Microsoft Entra ID administrator group id, activated if not set
4453
# MICROSOFT_ENTRA_ID_ADMINISTRATOR_GROUP_ID=
4554

46-
# Note: For Microsoft Entra ID, you need to:
47-
# 1. Create an app registration
48-
# - Go to Azure Portal -> Microsoft Entra ID -> App Registrations -> New Registration
49-
# - Fill in the name and select the supported account types
50-
# - Add a "Web" redirect URI: http://localhost:3000/api/users/oauth/microsoft-entra-id/callback
51-
# - When created, go to API Permissions -> Add a permission -> Microsoft Graph -> Delegated permissions -> Select the ones you need, e.g. email, openid, profile and offline_access -> Add permissions
52-
# - Optional: If you do not want users to have to give consent to your app everytime they login: Click on Grant admin consent for {tenant} -> Yes
53-
# - Optional: If you want groups to be part of your token(s), you can go to Token configuration -> Add groups claim -> Select the groups you want to add -> Save
54-
# - Go to Certificates & secrets -> Client secrets -> New client secret -> Add a description -> Expires -> Add -> Copy the secret (it will only be shown once) -> And save the secret somewhere safe or add it to your .env file
55-
# You can read a little about registering apps here as well: https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
56-
5755
################################################################################
5856
# Apple OAuth Config
5957
################################################################################
@@ -62,12 +60,3 @@ NEXT_PUBLIC_URL=http://localhost:3000
6260

6361
# Optional: Apple OAuth2 Client Secret (Generated from Apple Developer Portal)
6462
# APPLE_CLIENT_SECRET=your-generated-secret
65-
66-
# Note: For Apple OAuth, you need to:
67-
# 1. Create an App ID in Apple Developer Portal
68-
# Quick link: https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle
69-
# Long instruction
70-
# 2. Create a Services ID
71-
# 3. Configure domain association
72-
# 4. Generate a Client Secret
73-
# See: https://developer.apple.com/sign-in-with-apple/get-started/

examples/microsoft-entra-id.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0
1616
const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
1717
const microsoftGraphBaseUrl = "https://graph.microsoft.com/v1.0";
1818

19+
/**
20+
21+
Note: For Microsoft Entra ID, you need to xreate an app registration
22+
- Go to Azure Portal -> Microsoft Entra ID -> App Registrations -> New Registration
23+
- Fill in the name and select the supported account types
24+
- Add a "Web" redirect URI: http://localhost:3000/api/users/oauth/microsoft-entra-id/callback
25+
- When created, go to API Permissions -> Add a permission -> Microsoft Graph -> Delegated permissions -> Select the ones you need, e.g. email, openid, profile and offline_access -> Add permissions
26+
- Optional: If you do not want users to have to give consent to your app everytime they login: Click on Grant admin consent for {tenant} -> Yes
27+
- Optional: If you want groups to be part of your token(s), you can go to Token configuration -> Add groups claim -> Select the groups you want to add -> Save
28+
- Go to Certificates & secrets -> Client secrets -> New client secret -> Add a description -> Expires -> Add -> Copy the secret (it will only be shown once) -> And save the secret somewhere safe or add it to your .env file
29+
30+
You can read a little about registering apps here as well: https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
31+
*/
32+
1933
export const microsoftEntraIdOAuth = OAuth2Plugin({
2034
enabled:
2135
typeof clientId === "string" &&

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "payload-oauth2",
3-
"version": "1.0.16",
3+
"version": "1.0.17",
44
"type": "module",
55
"homepage:": "https://github.com/WilsonLe/payload-oauth2",
66
"repository": "https://github.com/WilsonLe/payload-oauth2",

src/callback-endpoint.ts

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
User,
1313
} from "payload";
1414
import { generatePayloadCookie, getFieldsToSign } from "payload";
15+
import { defaultCallbackExtractToken } from "./default-callback-extract-token";
1516
import { defaultGetToken } from "./default-get-token";
1617
import type { PluginOptions } from "./types";
1718

@@ -20,31 +21,6 @@ export const createCallbackEndpoint = (
2021
): Endpoint[] => {
2122
const handler: PayloadHandler = async (req: PayloadRequest) => {
2223
try {
23-
// Obtain code from either POST body or GET query parameters
24-
let code: string | undefined;
25-
if (req.method === "POST") {
26-
// Handle form data from POST request (used by Apple OAuth)
27-
const contentType = req.headers.get("content-type");
28-
if (contentType?.includes("application/x-www-form-urlencoded")) {
29-
const text = await (req as unknown as Request).text();
30-
const formData = new URLSearchParams(text);
31-
code = formData.get("code") || undefined;
32-
}
33-
} else if (req.method === "GET") {
34-
// Handle query parameters (used by Google OAuth)
35-
code =
36-
typeof req.query === "object" && req.query
37-
? (req.query as { code?: string }).code
38-
: undefined;
39-
}
40-
if (typeof code !== "string") {
41-
throw new Error(
42-
`Code not found in ${req.method === "POST" ? "body" : "query"}: ${
43-
req.method === "POST" ? "form-data" : JSON.stringify(req.query)
44-
}`,
45-
);
46-
}
47-
4824
// /////////////////////////////////////
4925
// shorthands
5026
// /////////////////////////////////////
@@ -61,6 +37,13 @@ export const createCallbackEndpoint = (
6137
!useEmailAsIdentity || pluginOptions.excludeEmailFromJwtToken || false;
6238
const onUserNotFoundBehavior =
6339
pluginOptions.onUserNotFoundBehavior || "create";
40+
const callbackExtractToken =
41+
pluginOptions.callbackExtractToken || defaultCallbackExtractToken;
42+
43+
// /////////////////////////////////////
44+
// extract code from request
45+
// /////////////////////////////////////
46+
const code = await callbackExtractToken(req);
6447

6548
// /////////////////////////////////////
6649
// beforeOperation - Collection

src/default-callback-extract-token.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { PayloadRequest } from "payload";
2+
3+
export const defaultCallbackExtractToken = async (
4+
req: PayloadRequest,
5+
): Promise<string> => {
6+
if (req.method === "POST") {
7+
// Handle form data from POST request (used by Apple OAuth)
8+
const contentType = req.headers.get("content-type");
9+
if (contentType?.includes("application/x-www-form-urlencoded")) {
10+
const text = await (req as unknown as Request).text();
11+
const formData = new URLSearchParams(text);
12+
const code = formData.get("code");
13+
if (typeof code === "string") {
14+
return code;
15+
} else {
16+
throw new Error(`Code not found in POST form data: ${text}`);
17+
}
18+
} else if (contentType?.includes("application/json")) {
19+
if (typeof req.json === "function") {
20+
const body = await req.json();
21+
if (typeof body.code === "string") {
22+
return body.code;
23+
} else {
24+
throw new Error(
25+
`Code not found in POST request body: ${JSON.stringify(body)}`,
26+
);
27+
}
28+
}
29+
} else {
30+
throw new Error(
31+
`Unsupported content-type: ${contentType} received in POST callback endpoint`,
32+
);
33+
}
34+
} else if (req.method === "GET") {
35+
// Handle query parameters (used by Google OAuth)
36+
if (typeof req.query === "object" && typeof req.query.code === "string") {
37+
return req.query.code;
38+
} else {
39+
throw new Error(
40+
`Code not found in GET request query param: ${JSON.stringify(req.query)}`,
41+
);
42+
}
43+
}
44+
throw new Error("Authorization code not found in callback request");
45+
};

src/default-get-token.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ export const defaultGetToken = async (
2222
const tokenData = await tokenResponse.json();
2323
const accessToken = tokenData?.access_token;
2424
if (typeof accessToken !== "string")
25-
throw new Error(`No access token: ${tokenData}`);
25+
throw new Error(`No access token: ${JSON.stringify(tokenData)}`);
2626
return accessToken;
2727
};

src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ export interface PluginOptions {
113113
req: PayloadRequest,
114114
) => Promise<Record<string, unknown>>;
115115

116+
/**
117+
* Function to extract authorization code from the callback request.
118+
* @param req PayloadRequest object
119+
* @returns Promise that resolves to the authorization code
120+
* @default `defaultCallbackExtractToken` in `src/default-callback-extract-token.ts`
121+
*/
122+
callbackExtractToken?: (req: PayloadRequest) => Promise<string>;
123+
116124
/**
117125
* Behavior when a user is not found in the database.
118126
* If set to "create", a new user will be created with the information

0 commit comments

Comments
 (0)