저번 포스팅에서 Google 로그인을 다뤘는데 이번엔 Apple 로그인에 대해 기록하려 한다. Apple의 정책 상 앱 심사를 할 때 다른 소셜 로그인이 적용되어있다면 Apple 로그인 또한 필수로 구현을 해야되기 때문에 Apple 로그인 방식에 대해 알아두면 추후에도 계속 사용할 수 있어서 유용하다. 만약 Google이나 카카오 로그인 등 소셜로그인을 지원하는데 Apple 로그인을 지원하지 않는다면 리젝을 받게 된다...ㅎ
Firebase Auth를 통해 Apple 로그인을 할 때는 아래와 같은 흐름으로 로그인이 진행된다.
1. developer 사이트에서 번들 추가하기 - Apple 개발자 계정 등록이 되어있어야 한다.
2. Capability 추가하기 - Sign in with Apple
3. Nonce 생성 후 Apple 로그인 요청하고, 해당 정보를 토대로 Credential(사용자 인증 정보) 생성
4. credential로 Firebase 로그인
우선 Apple 로그인을 사용하기 위해서는 Apple 개발자 계정이 있어야하고 해당 계정에서 번들을 추가해야한다. Apple 개발자 사이트에서 로그인을 하고 계정탭으로 들어가면 아래와 같은 화면을 볼 수 있는데 우선 여기서 식별자로 들어간다.
식별자로 들어가면 아래와 같은 화면이 보일건데 여기서 + 버튼을 눌러 새로운 식별자를 생성해주도록 한다.그러면 아래와 같이 화면이 보일거고, 여기서는 기존에 App IDs에 체크된 상태로 바로 Continue를 눌러 넘어가면 된다.
해당 화면에서도 동일하게 App이 체크된 상태로 Continue를 눌러 넘어간다.
아래와 같은 화면으로 들어가게 되면 우선 1번 자리에는 구분을 위해 앱 이름을 넣어주고 2번 자리에 애플 로그인을 넣고자 하는 프로젝트의 번들 ID를 넣어주면 된다.
그런 후 아래로 스크롤 하다보면 다음과 같이 Sign In with Apple이 보일건데 이걸 체크해주고 다시 최상단으로 돌아가 Continue -> register 버튼을 눌러주면 애플 로그인을 위한 번들을 추가할 수 있다.
다음으로는 Xcode에서 Capability를 추가해주면 된다.
우선 해당 프로젝트로 들어간 다음 앱 타겟에서 Signing & Capabilities 탭에 들어간 후 우측 상단에 + 버튼을 눌러준다.
sign을 검색하면 바로 Sign in with Apple을 찾을 수 있고 클릭해주면 Capability를 추가할 수 있다.
이제 코드 구현을 시작하면 되는데 Apple 로그인 버튼의 경우 애플에서 제공하는 AuthenticationServices을 Import 하게되면 SignInWithAppleButton이라는 걸 활용해 간편하게 구현할 수 있다.
import AuthenticationServices
SignInWithAppleButton { request in
// 인증 요청 시 불러지는 클로저
// request를 통해 원하는 정보와 Nonce 셋팅
} onCompletion: { result in
// 인증이 완료되었을 때 불러지는 클로저
// 현재 포스팅에서는 Firebase Auth에 로그인 해야되기 때문에
// 로그인에 성공한다면 해당 정보로 Firebase Auth에 로그인 하도록 구현 예정
}
Nonce란?
- 임의로 생성되는 암호화 토큰으로 재생 공격을 방지하는데 사용되는 랜덤 생성 토큰이다.
Firebase Auth를 활용해 Google 로그인을 할 때에는 GoogleAuthProvider를 활용해 credential을 생성하는데, 이때에는 idToken, accessToken이 필요했다. 참고 포스팅
반면 Apple 로그인의 경우 FirebaseAuth에서 제공하는 OAuthProvider를 활용해 credential을 만들어줘야 하는데 이때 Nonce가 필요하다. 그래서 Apple 로그인 시 request에 Nonce를 생성해서 넣어준 다음 Apple로그인을 하고 해당 Nonce로 credential을 만들어야 한다.
Nonce 생성과 암호화 과정은 Firebase Apple 로그인 문서 해당 문서를 보면 자세하게 나와있고, 아래 내용은 해당 문서를 참고해서 구현하였다.
func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
let charset: [Character] =
Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
var result = ""
var remainingLength = length
while remainingLength > 0 {
let randoms: [UInt8] = (0 ..< 16).map { _ in
var random: UInt8 = 0
let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
if errorCode != errSecSuccess {
fatalError(
"Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)"
)
}
return random
}
randoms.forEach { random in
if remainingLength == 0 {
return
}
if random < charset.count {
result.append(charset[Int(random)])
remainingLength -= 1
}
}
}
return result
}
위에서 보면 precondition이라는 것이 있는데 실제 앱을 출시했을 때에도 해당 전제조건이 만족하지 않으면 앱을 의도적으로 종료시켜야 하는 상황에 사용한다. 만약 해당 조건을 만족하지 않았는데 앱이 실행되었을 때 서버와의 통신이 잘못되어 데이터베이스에 잘못된 값이 저장된다면 많은 문제를 야기할 수 있기 때문에 이런 경우 사용한다.
precondition은 디버깅 모드, 배포되는 애플리케이션 모두에서 동작하고, production에서 이슈를 발견하는데 유용하다.
각설하고, 위의 방식으로 Nonce를 생성한 다음 sha256을 통해 해싱하면 된다.
func sha256(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap {
String(format: "%02x", $0)
}.joined()
return hashString
}
Nonce 생성 및 해싱 메서드까지 구현하였다면 해당 메서드들을 이용해 암호화된 Nonce를 생성하는 메서드를 구현하면 된다.
참고로 ASAuthrizationAppleIDRequest를 사용하려면 AuthenticationServices를 Import 해줘야 한다.
func handleSignInWithAppleRequest(_ request: ASAuthorizationAppleIDRequest) -> String {
request.requestedScopes = [.fullName, .email]
let nonce = randomNonceString()
request.nonce = sha256(nonce)
return nonce
}
여기까지 진행했다면 ViewModel에서 위의 메서드를 활용해 request를 이용한 Nonce를 생성하면 된다.
SignInWithAppleButton { request in
// 여기서 nonce를 생성
self.nonce = handleSignInWithAppleRequest(request)
} onCompletion: { result in
// 다음에서 추가 될 예정
}
Apple 로그인도 Google 로그인과 마찬가지로 Combine을 지원하지 않기 때문에 Completion Handler를 이용해 로그인을 처리하고, Future를 이용해 Publisher를 생성할 예정이다.
private func handleSignInWithAppleCompletion(_ authorization: ASAuthorization,
nonce: String,
completion: @escaping (Result<User, Error>) -> Void) {
// idCredential 추출
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential,
let appleIDToken = appleIDCredential.identityToken else {
completion(.failure(AuthenticationError.tokenError))
return
}
// idToken 추출
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
completion(.failure(AuthenticationError.tokenError))
return
}
// credential 생성
let credential = OAuthProvider.credential(withProviderID: "apple.com",
idToken: idTokenString,
rawNonce: nonce)
// google 로그인 포스팅에서 구현한 Firebase 로그인 메서드를 활용해 Firebase Auth 로그인
authenticateUserWithFirebase(credential: credential) { result in
// 만약 성공할 경우 appleIDCredential에서 제공하는 성과 이름을 합쳐서 이름으로 변환하고
// success로 결과값 반환
// 실패할 경우 error 반환
switch result {
case var .success(user):
user.name = [appleIDCredential.fullName?.givenName, appleIDCredential.fullName?.familyName]
.compactMap { $0 }
.joined(separator: " ")
completion(.success(user))
case let .failure(error):
completion(.failure(error))
}
}
}
위 메서드는 내부에서 사용하는 로그인 메서드이고 이제 해당 결과를 Future를 이용해 Publisher를 생성하는 메서드를 구현해야 한다.
func handleSignInWithAppleCompletion(_ authorization: ASAuthorization, nonce: String) -> AnyPublisher<User, ServiceError> {
Future { [weak self] promise in
self?.handleSignInWithAppleCompletion(authorization, nonce: nonce) { result in
switch result {
case let .success(user):
promise(.success(user))
case let .failure(error):
promise(.failure(.error(error)))
}
}
}.eraseToAnyPublisher()
}
이제 마지막으로 구현한 메서드를 앞에서 만들었던 Apple 버튼에 넣어주도록 한다.
SignInWithAppleButton { request in
// 여기서 nonce를 생성
self.nonce = handleSignInWithAppleRequest(request)
} onCompletion: { result in
if case let .success(authorization) = result {
guard let nonce = nonce else { return }
handleSignInWithAppleCompletion(authorization, nonce: nonce)
.sink { [weak self] completion in
if case .failure = completion {
// 로그인 실패시 로직
}
} receiveValue: { [weak self] user in
// 로그인 성공 시 로직
}.store(in: &subscriptions)
} else if case let .failure(error) = result {
print(error.localizedDescription)
}
}
여기까지 완료한다면 Apple Login 연동은 완료된다.
전체 코드는 Messenger Demo App 해당 프로젝트의 AuthenticationService에서 확인할 수 있다.