-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathapple.ts
More file actions
167 lines (151 loc) · 7.05 KB
/
Copy pathapple.ts
File metadata and controls
167 lines (151 loc) · 7.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import { PayloadRequest } from "payload";
import { OAuth2Plugin } from "../src/index";
/**
To setup Apple OAuth, refer to official documentation:
https://developer.apple.com/sign-in-with-apple/get-started/
However, the process is quite complex and requires several steps, so here's a quick start guide:
To setup Apple OAuth in Payload CMS, you need the following 4 values:
1. APPLE_CLIENT_ID: Your Service ID from the Apple Developer portal (e.g. com.example.myapp)
2. APPLE_CLIENT_SECRET: Your client secret, which is a JWT signed with your private key. This requires value 3 and 4 to generate:
3. APPLE_KEY_ID: The Key ID from the Apple Developer portal
4. APPLE_TEAM_ID: Your Apple Developer Team ID, which can be found in the Apple Developer portal.
Prerequisites: Have a valid Apple Developer account and access to the Apple Developer portal.
1. Create an App ID in the Apple Developer portal
- Quick links: https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle
- Step by step instruction:
> https://developer.apple.com/account
> Certificates, IDs & Profiles
> Identifiers
> Create new identifiders
> Select App IDs
> Select App
> Arbitrary description, explicit bundle ID (e.g. com.example.myapp)
> Capabilities: Enable Sign In with Apple > Save (ignore Server-to-Server Notification Endpoint)
> Continue/Register
2. Create a service ID in the Apple Developer portal
- Quick links: https://developer.apple.com/account/resources/identifiers/serviceId/add
- Step by step instruction:
> https://developer.apple.com/account
> Certificates, IDs & Profiles
> Identifiers
> Create new identifiders
> Select Service IDs
> Arbitrary description, identifier (e.g. com.example.myapp.si) - IMPORTANT, I have found that this must be a subdomain of your app's bundle ID, notice the ".si" suffix.
> Continue/Register
> Value (1) APPLE_CLIENT_ID should be the identifier you just created (e.g. com.example.myapp.si)
3. Create a new key in the Apple Developer portal
- Quick links: https://developer.apple.com/account/resources/authkeys/add
- Step by step instruction:
> https://developer.apple.com/account
> Certificates, IDs & Profiles
> Keys
> Create new key
> Arbitrary key name and key usage description.
> Enable Sign In with Apple
> Configure
> Select the App ID you created in step 1
> Continue/Register
> Download the key file, which is a .p8 file. This file contains your private key.
> Value (3) APPLE_KEY_ID is the Key ID from the key you just created.
4. Obtain your Apple Developer Team ID
> https://developer.apple.com/account
> Membership details
> Your Team ID is listed there, this should be value (4) APPLE_TEAM_ID.
5. Based on value (3) APPLE_KEY_ID, value (4) APPLE_TEAM_ID and the private key file you downloaded in step 3, generate value (2) APPLE_CLIENT_SECRET by running:
```sh
pnpm payload-oauth2:generate-apple-client-secret --team-id 4659F6UUC3 --client-id com.example.app.sso --key-id XXXXXXXXXX --private-key-path AuthKey_XXXXXXXXXX.p8
```
In the example below:
- `process.env.APPLE_CLIENT_ID` is (1) APPLE_CLIENT_ID
- `process.env.APPLE_CLIENT_SECRET` is (2) APPLE_CLIENT_SECRET,
Dev Note:
- I consistently got an `invalid_client` error when redirecting to `https://appleid.apple.com/auth/authorize`. I noticed that newly generated keys took 2 days for it to go into effect. After waiting for 2 days, the error went away.
- For web, I noticed that service id works if it is a subdomain of the app's bundle id. For example, if your app's bundle id is `com.example.myapp`, then your service id must be something like `com.example.myapp.sso`. I tried to use a service id that is not a subdomain of the app's bundle id, I got an `invalid_client` error when redirecting to `https://appleid.apple.com/auth/authorize`.
*/
////////////////////////////////////////////////////////////////////////////////
// Apple OAuth
////////////////////////////////////////////////////////////////////////////////
export const appleOAuth = OAuth2Plugin({
enabled:
typeof process.env.APPLE_CLIENT_ID === "string" &&
typeof process.env.APPLE_TEAM_ID === "string" &&
typeof process.env.APPLE_KEY_ID === "string" &&
(typeof process.env.APPLE_CLIENT_SECRET === "string" ||
typeof process.env.APPLE_CLIENT_AUTH_KEY_CONTENT === "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: PayloadRequest, token?: string) => {
return "/admin";
},
failureRedirect: (req, err) => {
req.payload.logger.error(err);
return `/admin/login?error=${JSON.stringify(err)}`;
},
});