-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAppleAuthenticationService.swift
More file actions
286 lines (233 loc) · 10.8 KB
/
AppleAuthenticationService.swift
File metadata and controls
286 lines (233 loc) · 10.8 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
//
// AppleAuthenticationService.swift
// DevLog
//
// Created by opfic on 6/4/25.
//
import AuthenticationServices
import CryptoKit
import FirebaseAuth
import FirebaseFirestore
import FirebaseFunctions
import FirebaseMessaging
import Foundation
final class AppleAuthenticationService: AuthenticationService {
private enum FunctionName: String {
case requestAppleCustomToken
case refreshAppleAccessToken
case requestAppleRefreshToken
case revokeAppleAccessToken
}
private var appleSignInDelegate: AppleSignInDelegate?
private let store = Firestore.firestore()
private let functions = Functions.functions(region: "asia-northeast3")
private let messaging = Messaging.messaging()
private var user: User? { Auth.auth().currentUser }
private let providerID = AuthProviderID.apple
private let logger = Logger(category: "AppleAuthService")
func signIn() async throws -> AuthDataResponse {
logger.info("Starting Apple sign in")
do {
let response = try await authenticateWithAppleAsync()
let nonce = response.nonce
let credential = response.credential
let authorizationCode = response.authorizationCode
let idTokenString = response.idTokenString
// Firebase Function을 통해 customToken 요청
logger.debug("Requesting custom token from Firebase Function")
let customToken = try await requestAppleCustomToken(
idToken: idTokenString,
authorizationCode: authorizationCode
)
// customToken으로 Firebase 로그인
logger.debug("Signing in with custom token")
let result = try await Auth.auth().signIn(withCustomToken: customToken)
let changeRequest = result.user.createProfileChangeRequest()
var displayName: String?
// 최초 사용자 가입 시 사용자 이름 설정
if let fullName = credential.fullName {
let formatter = PersonNameComponentsFormatter()
formatter.style = .long
let formattedName = formatter.string(from: fullName)
if !formattedName.isEmpty {
displayName = formattedName
}
}
// 이미 가입된 사용자일 경우 Firestore에서 사용자 이름 가져오기
if displayName == nil {
let doc = try await store
.document(FirestorePath.userData(result.user.uid, document: .info))
.getDocument()
displayName = doc.data()?["appleName"] as? String
}
// FirebaseAuth 사용자 프로필 업데이트
changeRequest.displayName = displayName ?? ""
changeRequest.photoURL = nil // Apple ID 프로필 사진 URL은 제공되지 않음
try await changeRequest.commitChanges()
// FirebaseAuth 계정에 Apple ID 연결
if !result.user.providerData.contains(where: { $0.providerID == providerID.rawValue }) {
let appleCredential = OAuthProvider.credential(
providerID: providerID,
idToken: idTokenString,
rawNonce: nonce
)
try await result.user.link(with: appleCredential)
}
let fcmToken = try await messaging.token()
logger.info("Successfully signed in with Apple")
return result.user.makeResponse(providerID: .apple, fcmToken: fcmToken)
} catch {
logger.error("Failed to sign in with Apple", error: error)
throw error
}
}
func signOut(_ uid: String) async throws {
do {
let infoRef = store.document(FirestorePath.userData(uid, document: .tokens))
let doc = try await infoRef.getDocument()
if doc.exists {
try await infoRef.updateData(["fcmToken": FieldValue.delete()])
}
try await messaging.deleteToken()
try Auth.auth().signOut()
} catch {
logger.error("Failed to sign out with Apple", error: error)
throw error
}
}
func deleteAuth(_ uid: String) async throws {
do {
let token = try await refreshAppleAccessToken()
try await revokeAppleAccessToken(token: token)
} catch {
logger.error("Failed to delete Apple auth", error: error)
throw error
}
}
func link(uid: String, email: String) async throws {
do {
let response = try await authenticateWithAppleAsync()
let nonce = response.nonce
let credential = response.credential
let authorizationCode = response.authorizationCode
let idTokenString = response.idTokenString
let refreshToken = try await requestAppleRefreshToken(uid: uid, authorizationCode: authorizationCode)
guard let appleEmail = credential.email else {
try await revokeAppleAccessToken(token: refreshToken)
throw EmailFetchError.emailNotFound
}
if appleEmail != email {
try await revokeAppleAccessToken(token: refreshToken)
throw EmailFetchError.emailMismatch
}
let appleCredential = OAuthProvider.credential(
providerID: providerID,
idToken: idTokenString,
rawNonce: nonce
)
try await user?.link(with: appleCredential)
} catch {
logger.error("Failed to link Apple account", error: error)
throw error
}
}
func unlink(_ uid: String) async throws {
do {
logger.info("Starting Apple access token refresh for unlink. uid: \(uid)")
let accessToken = try await refreshAppleAccessToken()
logger.info("Starting Apple access token revocation for unlink. uid: \(uid)")
try await revokeAppleAccessToken(token: accessToken)
let tokensRef = store.document(FirestorePath.userData(uid, document: .tokens))
logger.info("Starting Apple token document fetch for unlink. uid: \(uid)")
let doc = try await tokensRef.getDocument()
if doc.exists {
logger.info("Starting Apple refresh token deletion from Firestore for unlink. uid: \(uid)")
try await tokensRef.updateData([
"appleRefreshToken": FieldValue.delete()
])
}
logger.info("Starting Firebase Apple provider unlink. uid: \(uid)")
_ = try await user?.unlink(fromProvider: providerID.rawValue)
} catch {
logger.error("Failed to unlink Apple account", error: error)
throw error
}
}
// Apple 인증 메서드
@MainActor
func authenticateWithAppleAsync() async throws -> AppleAuthResponse {
// 자체 nonce 생성 및 해시화
let nonce = UUID().uuidString
let hashedNonce = SHA256.hash(data: Data(nonce.utf8)).map { String(format: "%02x", $0) }.joined()
let provider = ASAuthorizationAppleIDProvider()
let request = provider.createRequest()
request.requestedScopes = [.fullName, .email] // 사용자 정보 요청
request.nonce = hashedNonce // Apple API는 SHA256 해시값을 요구함
let controller = ASAuthorizationController(authorizationRequests: [request])
let authorization = try await withCheckedThrowingContinuation { continuation in
self.appleSignInDelegate = AppleSignInDelegate(continuation: continuation)
controller.delegate = self.appleSignInDelegate
controller.presentationContextProvider = self.appleSignInDelegate
controller.performRequests()
}
// Apple ID 인증 결과 처리
guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
let appleIdToken = credential.identityToken,
let authorizationCode = credential.authorizationCode,
let idTokenString = String(data: appleIdToken, encoding: .utf8) else {
throw URLError(.badServerResponse)
}
return AppleAuthResponse(
nonce: nonce,
credential: credential,
authorizationCode: authorizationCode,
idTokenString: idTokenString
)
}
// Apple CustomToken 발급 메서드
private func requestAppleCustomToken(idToken: String, authorizationCode: Data) async throws -> String {
guard let authorizationCode = String(data: authorizationCode, encoding: .utf8) else {
throw URLError(.badServerResponse)
}
let requestTokenFunction = functions.httpsCallable(FunctionName.requestAppleCustomToken)
let result = try await requestTokenFunction.call([
"idToken": idToken,
"authorizationCode": authorizationCode
])
if let data = result.data as? [String: Any], let customToken = data["customToken"] as? String {
return customToken
}
throw URLError(.badServerResponse)
}
// Apple AceessToken 재발급 메서드
private func refreshAppleAccessToken() async throws -> String {
let refreshFunction = functions.httpsCallable(FunctionName.refreshAppleAccessToken)
let result = try await refreshFunction.call()
guard let data = result.data as? [String: Any],
let accessToken = data["token"] as? String else {
throw URLError(.cannotParseResponse)
}
return accessToken
}
// Apple RefreshToken 발급 메서드
func requestAppleRefreshToken(uid: String, authorizationCode: Data) async throws -> String {
guard let authorizationCode = String(data: authorizationCode, encoding: .utf8) else {
throw URLError(.userAuthenticationRequired)
}
let requestFuction = functions.httpsCallable(FunctionName.requestAppleRefreshToken)
let params: [String: Any] = [
"authorizationCode": authorizationCode,
"uid": uid
]
let result = try await requestFuction.call(params)
if let data = result.data as? [String: Any], let accessToken = data["refreshToken"] as? String {
return accessToken
}
throw URLError(.badServerResponse)
}
// Apple AccessToken 취소 메서드
func revokeAppleAccessToken(token: String) async throws {
let revokeFunction = functions.httpsCallable(FunctionName.revokeAppleAccessToken)
_ = try await revokeFunction.call(["token": token])
}
}