-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapple.ts
More file actions
376 lines (330 loc) · 13.3 KB
/
Copy pathapple.ts
File metadata and controls
376 lines (330 loc) · 13.3 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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
import {onCall, HttpsError} from "firebase-functions/v2/https";
import * as admin from "firebase-admin";
import axios from "axios";
import * as jwt from "jsonwebtoken";
// Apple ID 토큰 페이로드 인터페이스 정의
interface AppleTokenPayload {
iss: string; // 발행자 (issuer)
sub: string; // 사용자 ID (subject)
aud: string; // 앱 ID (audience)
iat: number; // 발행 시간 (issued at)
exp: number; // 만료 시간 (expiration)
email?: string; // 사용자 이메일 (선택적)
email_verified?: string; // 이메일 인증 여부
is_private_email?: boolean; // 개인정보 보호 이메일 여부
nonce?: string; // 보안용 난수값
nonce_supported?: boolean;
real_user_status?: number; // 실제 사용자 상태
auth_time?: number; // 인증 시간
}
function getAppleConfiguration() {
const teamId = process.env.APPLE_TEAM_ID;
const clientId = process.env.APPLE_CLIENT_ID;
const keyId = process.env.APPLE_KEY_ID;
const privateKey = (process.env.APPLE_PRIVATE_KEY || "").replace(/\\n/g, "\n");
const configs = {
APPLE_TEAM_ID: teamId,
APPLE_CLIENT_ID: clientId,
APPLE_KEY_ID: keyId,
APPLE_PRIVATE_KEY: privateKey
};
const missingKeys = Object.entries(configs)
.filter(([, value]) => !value)
.map(([key]) => key);
if (0 < missingKeys.length) {
console.error("Missing Apple configuration", {
missingKeys
});
throw new HttpsError(
'internal',
`Missing Apple configuration for: ${missingKeys.join(", ")}`
);
}
return {
teamId: teamId!,
clientId: clientId!,
keyId: keyId!,
privateKey: privateKey!
};
}
export const requestAppleCustomToken = onCall({
cors: true,
maxInstances: 10,
region: "asia-northeast3",
}, async (request) => {
try {
const { idToken, authorizationCode } = request.data;
if (!idToken || !authorizationCode) {
throw new HttpsError('invalid-argument', 'ID token and authorization code are required');
}
// // 1. Verify and decode the Apple ID token
let decodedToken: AppleTokenPayload;
try {
decodedToken = jwt.decode(idToken) as AppleTokenPayload;
if (!decodedToken) {
throw new HttpsError('invalid-argument', 'Invalid Apple ID token');
}
} catch (error) {
console.error('Error decoding Apple ID token:', error);
throw new HttpsError('invalid-argument', 'Failed to decode Apple ID token');
}
// 2. Get user information from the decoded token
const userId = decodedToken.sub; // Apple's unique user ID
const email = decodedToken.email;
if (!userId) {
throw new HttpsError('internal', 'Could not get user ID from Apple token');
}
// 3. Find or create Firebase user
let uid;
try {
// 애플에서 받아오는 토큰에서 이메일이 존재하는 경우
if (email) {
try {
const userRecord = await admin.auth().getUserByEmail(email);
uid = userRecord.uid;
console.log(`Found existing user by email (${email})`);
} catch (error) {
// User not found by email, create new user
const userRecord = await admin.auth().createUser({
email: email,
emailVerified: decodedToken.email_verified === 'true',
});
uid = userRecord.uid;
console.log(`Created new user with email: ${uid}`);
}
}
else {
// 애플 정책 변환 또는 알수 없는 이유로 이메일을 못받아오는 경우
try {
const userRecord = await admin.auth().getUser(`apple:${userId}`);
uid = userRecord.uid;
} catch (error) {
// User not found, create new user with Apple UID
const userRecord = await admin.auth().createUser({});
uid = userRecord.uid;
console.log(`Created new user with Apple ID: ${uid}`);
}
}
// 4. Save refresh token to Firestore
const refreshToken = await requestAppleRefreshTokenHelper(authorizationCode);
await admin.firestore().collection("users").doc(uid).collection("userData").doc("tokens").set({
appleRefreshToken: refreshToken
}, { merge: true });
// 5. Create Firebase custom token
const customToken = await admin.auth().createCustomToken(uid);
return {
customToken
};
} catch (error) {
console.error('Error processing Apple authentication:', error);
throw new HttpsError('internal', error instanceof Error ? error.message : 'Unknown error occurred during authentication');
}
} catch (error) {
console.error('Apple custom token creation error:', error);
throw new HttpsError('internal', error instanceof Error ? error.message : 'Unknown error occurred');
}
});
export const requestAppleRefreshToken = onCall({
cors: true,
maxInstances: 10,
region: "asia-northeast3",
}, async (request) => {
if (!request.auth) {
throw new HttpsError("unauthenticated", "Authentication required");
}
try {
// 요청 데이터가 null인지 확인
console.log("Request data:", request.data);
if (!request.data) {
throw new HttpsError(
"invalid-argument",
"Request data is missing"
);
}
const { authorizationCode, uid } = request.data;
if (!authorizationCode || !uid) {
throw new HttpsError("invalid-argument", "Authorization code and uid are required");
}
const refreshToken = await requestAppleRefreshTokenHelper(authorizationCode);
console.log("appleRefreshToken:", refreshToken);
// Apple 서버에서 받은 응답을 확인
await admin.firestore().collection("users").doc(uid).collection("userData").doc("tokens").set({
appleRefreshToken: refreshToken
}, { merge: true });
return {
success: true,
refreshToken: refreshToken
};
} catch (error) {
console.error("Error request Apple refresh token:", error);
throw new HttpsError("internal", "Failed to process Apple sign in");
}
});
export const refreshAppleAccessToken = onCall({
cors: true,
maxInstances: 10,
region: "asia-northeast3",
}, async (request) => {
// 인증 확인
if (!request.auth) {
throw new HttpsError("unauthenticated", "Authentication required");
}
try {
const uid = request.auth.uid;
// 클라이언트 경로에서만 시도
console.log(`Fetching from collection(${uid})/doc(info)`);
const userDoc = await admin.firestore().collection("users").doc(uid).collection("userData").doc("tokens").get();
if (!userDoc.exists) {
console.error(`User document not found for ID: ${uid}`);
throw new HttpsError("not-found", `User document not found at collection('/users/${uid}')/userData/doc('info')`);
}
const userData = userDoc.data();
const refreshToken = userData?.appleRefreshToken;
if (!refreshToken) {
console.error("User document exists but has no appleRefreshToken field:", userData);
throw new HttpsError("not-found", "Apple refresh token not found for this user");
}
console.log("Successfully retrieved refresh token from Firestore");
const { teamId, clientId, keyId, privateKey } = getAppleConfiguration();
// Create client_secret JWT
const clientSecret = jwt.sign({}, privateKey, {
algorithm: "ES256",
expiresIn: "5m",
audience: "https://appleid.apple.com",
issuer: teamId,
subject: clientId,
keyid: keyId,
});
// Request new access token from Apple
const response = await axios.post<{access_token: string}>("https://appleid.apple.com/auth/token",
new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
grant_type: "refresh_token",
refresh_token: refreshToken,
}).toString(), {
headers: {"Content-Type": "application/x-www-form-urlencoded"},
}
);
// Return the new access token
if (response.data && response.data.access_token) {
return {token: response.data.access_token}; // v2에서는 객체로 반환
} else {
throw new HttpsError(
"internal",
"Failed to retrieve access token from Apple response."
);
}
} catch (error: unknown) {
console.error("Error refreshing Apple token:", error);
if ((axios as any).isAxiosError(error)) {
console.error("Axios error details:", (error as any).response?.data);
throw new HttpsError("internal",
`Token refresh failed: ${
(error as any).response?.data?.error || (error as Error).message
}`
);
} else if (error instanceof Error) {
throw new HttpsError("internal", `Token refresh error: ${error.message}`);
} else {
throw new HttpsError("internal", "An unknown error occurred during token refresh.");
}
}
});
export const revokeAppleAccessToken = onCall({
cors: true,
maxInstances: 10,
region: "asia-northeast3",
}, async (request) => {
// 인증 확인
if (!request.auth) {
throw new HttpsError("unauthenticated", "Authentication required");
}
try {
const { token } = request.data;
if (!token) {
throw new HttpsError("invalid-argument", "Token is required");
}
console.log("Starting Apple token revocation", {
uid: request.auth.uid
});
console.log("Starting Apple configuration load for token revocation");
const { teamId, clientId, keyId, privateKey } = getAppleConfiguration();
console.log("Starting Apple client secret creation for token revocation");
const clientSecret = jwt.sign({}, privateKey, {
algorithm: "ES256",
expiresIn: "5m",
audience: "https://appleid.apple.com",
issuer: teamId,
subject: clientId,
keyid: keyId,
});
console.log("Starting Apple revoke API request");
await axios.post(
"https://appleid.apple.com/auth/revoke",
new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
token: token,
token_type_hint: "access_token" // access_token 또는 refresh_token 지정 가능
}).toString(), {
headers: {"Content-Type": "application/x-www-form-urlencoded"},
});
return { success: true };
} catch (error: unknown) {
console.error("Error revoking Apple token:", error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((axios as any).isAxiosError(error)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
console.error("Axios error details:", (error as any).response?.data);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
throw new HttpsError(
"internal",
`Token revocation failed: ${
(error as any).response?.data?.error ||
(error as Error).message
}`);
} else if (error instanceof Error) {
throw new HttpsError(
"internal",
`Token revocation error: ${error.message}`);
} else {
throw new HttpsError(
"internal",
"An unknown error occurred during token revocation.");
}
}
});
export async function requestAppleRefreshTokenHelper(authorizationCode: string): Promise<string> {
const { teamId, clientId, keyId, privateKey } = getAppleConfiguration();
// JWT 생성
const clientSecret = jwt.sign({}, privateKey, {
algorithm: "ES256",
expiresIn: "5m",
audience: "https://appleid.apple.com",
issuer: teamId,
subject: clientId,
keyid: keyId,
});
// Apple 서버에 토큰 요청 (authorization_code 사용)
const tokenResponse = await axios.post<{
access_token: string,
refresh_token: string,
id_token: string,
token_type: string,
expires_in: number
}>("https://appleid.apple.com/auth/token",
new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
code: authorizationCode,
grant_type: "authorization_code",
}).toString(), {
headers: {"Content-Type": "application/x-www-form-urlencoded"},
});
const refreshToken = tokenResponse.data.refresh_token;
if (!refreshToken) {
throw new HttpsError('internal', 'Apple에서 refresh_token을 받아오지 못했습니다.');
}
return refreshToken;
}