Skip to content

Commit 97cd124

Browse files
authored
Merge pull request #25 from omnimaxxing/feature/apple-oauth-support
feat: add Apple OAuth support with response_mode parameter
2 parents 2f864e3 + 65ccf8d commit 97cd124

File tree

6 files changed

+259
-61
lines changed

6 files changed

+259
-61
lines changed

dev/.env.example

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,19 @@ NEXT_PUBLIC_URL=http://localhost:3000
2727

2828
# optional: google oauth2 client secret, not activated if not set
2929
# ZITADEL_CLIENT_SECRET=
30+
31+
################################################################################
32+
# Apple OAuth Config
33+
################################################################################
34+
# Optional: Apple OAuth2 Client ID (Services ID), not activated if not set
35+
# APPLE_CLIENT_ID=com.your.app.id
36+
37+
# Optional: Apple OAuth2 Client Secret (Generated from Apple Developer Portal)
38+
# APPLE_CLIENT_SECRET=your-generated-secret
39+
40+
# Note: For Apple OAuth, you need to:
41+
# 1. Create an App ID in Apple Developer Portal
42+
# 2. Create a Services ID
43+
# 3. Configure domain association
44+
# 4. Generate a Client Secret
45+
# See: https://developer.apple.com/sign-in-with-apple/get-started/

examples/apple.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { PayloadRequest } from "payload";
2+
import { OAuth2Plugin } from "../src/index";
3+
4+
////////////////////////////////////////////////////////////////////////////////
5+
// Apple OAuth
6+
////////////////////////////////////////////////////////////////////////////////
7+
export const appleOAuth = OAuth2Plugin({
8+
enabled:
9+
typeof process.env.APPLE_CLIENT_ID === "string" &&
10+
typeof process.env.APPLE_CLIENT_SECRET === "string",
11+
strategyName: "apple",
12+
useEmailAsIdentity: true,
13+
serverURL: process.env.NEXT_PUBLIC_URL || "http://localhost:3000",
14+
clientId: process.env.APPLE_CLIENT_ID || "",
15+
clientSecret: process.env.APPLE_CLIENT_SECRET || "",
16+
authorizePath: "/oauth/apple",
17+
callbackPath: "/oauth/apple/callback",
18+
authCollection: "users",
19+
tokenEndpoint: "https://appleid.apple.com/auth/token",
20+
scopes: ["name", "email"],
21+
providerAuthorizationUrl: "https://appleid.apple.com/auth/authorize",
22+
// Required for Apple OAuth when requesting name or email scopes
23+
responseMode: "form_post",
24+
getUserInfo: async (accessToken: string, req: PayloadRequest) => {
25+
try {
26+
// For Apple, the ID token is a JWT that contains user info
27+
const tokenParts = accessToken.split(".");
28+
if (tokenParts.length !== 3) {
29+
throw new Error("Invalid ID token format");
30+
}
31+
32+
// Decode the base64 payload
33+
const payload = JSON.parse(Buffer.from(tokenParts[1], "base64").toString());
34+
35+
if (!payload.email) {
36+
throw new Error("No email found in payload");
37+
}
38+
39+
return {
40+
email: payload.email,
41+
sub: payload.sub,
42+
// Apple provides name only on first login
43+
firstName: payload.given_name || "",
44+
lastName: payload.family_name || "",
45+
};
46+
} catch (error) {
47+
req.payload.logger.error("Error parsing Apple token:", error);
48+
throw error;
49+
}
50+
},
51+
getToken: async (code: string, req: PayloadRequest) => {
52+
try {
53+
const redirectUri = `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}/api/users/oauth/apple/callback`;
54+
55+
// Make the token exchange request
56+
const params = new URLSearchParams({
57+
client_id: process.env.APPLE_CLIENT_ID || "",
58+
client_secret: process.env.APPLE_CLIENT_SECRET || "",
59+
code: code,
60+
grant_type: "authorization_code",
61+
redirect_uri: redirectUri,
62+
});
63+
64+
const response = await fetch("https://appleid.apple.com/auth/token", {
65+
method: "POST",
66+
headers: {
67+
"Content-Type": "application/x-www-form-urlencoded",
68+
},
69+
body: params.toString(),
70+
});
71+
72+
if (!response.ok) {
73+
const error = await response.text();
74+
throw new Error(`Token exchange failed: ${error}`);
75+
}
76+
77+
const tokenResponse = await response.json();
78+
79+
// Return the id_token which contains the user info
80+
return tokenResponse.id_token;
81+
} catch (error) {
82+
req.payload.logger.error("Error in getToken:", error);
83+
throw error;
84+
}
85+
},
86+
successRedirect: (req) => {
87+
// Check user roles to determine redirect
88+
const user = req.user;
89+
if (user && Array.isArray(user.roles)) {
90+
if (user.roles.includes("admin")) {
91+
return "/admin";
92+
}
93+
}
94+
return "/"; // Default redirect for customers
95+
},
96+
failureRedirect: (req, err) => {
97+
req.payload.logger.error(err);
98+
return "/login?error=apple-auth-failed";
99+
},
100+
});

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "module",
55
"homepage:": "https://payloadcms.com",
66
"repository": "https://github.com/WilsonLe/payload-oauth2",
7-
"description": "OAuth2 plugin for Payload CMS",
7+
"description": "OAuth2 plugin for Payload CMS with Apple Sign In support",
88
"main": "dist/index.js",
99
"types": "dist/index.d.ts",
1010
"keywords": [
@@ -14,7 +14,8 @@
1414
"typescript",
1515
"react",
1616
"oauth2",
17-
"payload-plugin"
17+
"payload-plugin",
18+
"apple-sign-in"
1819
],
1920
"files": [
2021
"dist"

src/authorize-endpoint.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Endpoint } from "payload";
2-
import { PluginTypes } from "./types";
1+
import type { Endpoint } from "payload";
2+
import type { PluginTypes } from "./types";
33

44
export const createAuthorizeEndpoint = (
55
pluginOptions: PluginTypes,
@@ -20,7 +20,15 @@ export const createAuthorizeEndpoint = (
2020

2121
const responseType = "code";
2222
const accessType = "offline";
23-
const authorizeUrl = `${pluginOptions.providerAuthorizationUrl}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=${responseType}&access_type=${accessType}${prompt}`;
23+
24+
// Add response_mode if specified (required for Apple OAuth with name/email scopes)
25+
const responseMode = pluginOptions.responseMode
26+
? `&response_mode=${pluginOptions.responseMode}`
27+
: "";
28+
29+
const authorizeUrl = `${
30+
pluginOptions.providerAuthorizationUrl
31+
}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=${responseType}&access_type=${accessType}${prompt}${responseMode}`;
2432

2533
return Response.redirect(authorizeUrl);
2634
},

0 commit comments

Comments
 (0)