4. App ID 등록
Apple 개발자 페이지로 이동하여 인증서, 식별자 및 프로파일을 클릭하여 이동한다.
Identifiers 에서 + 버튼을 누르고 AppID 등록을 한다.
Description에는 App 이름을 적고, Bundle ID는 Xcode에서 프로젝트 파일에서 볼 수 있는 Bundle ID와 일치하게 적어주면 된다.
아래의 Capabilities를 살펴보면, 항목중에 Sign in with Apple을 찾아서 체크박스를 체크해준다.
모두 작성하고 Continue 를 누르고 Register를 한 번 더 누르면 AppID가 생성된다.
Service ID 등록
그후 다시 Identifiers 탭에서 +버튼을 다시 누른다.
이번엔 Service ID를 선택하고 Continue를 누른다
Description에는 FirebaseLogin 기능을 뜻하는 이름을 지어주고, com.domainname.appname을 되도록 지키는 선으로 ID를 지어준다.
Continue를 누르고 Register를 한번 더 눌러 등록한다.
Identifier 탭에서 우측의 검색 버튼을 누르고, Service ID로 이동한다. 5번에서 만든 Service ID를 클릭하여 들어가면 아래에 Sign in with Apple 이 보인다.
Configure를 누르면
이러한 창이 뜨는데, Primary App ID는 4번에서 만든 App ID가 등록되어 있을 테니 넣어주면 된다. 이제 다시 Firebase Console의 Authentication으로 이동하고 설정에서 승인된 도메인을 찾는다.
-> Domains and Subdomains 칸에 넣어주면 된다.
Firebase Console의 Authentication으로 이동하고 로그인 방법에서 Apple을 클릭한다.
-> Return URLs에 넣어주면 된다.
이렇게하면 애플 로그인을 위한 세팅은 끝난다.
우선 LoginVC에 apple login 관련 라이브러리를 주입해준다.
import AuthenticationServices // apple login 관련 라이브러리
import CryptoKit // 해시 값 추가
Apple 로그인 관련 코드는 한 번에 이해하기 어려워, 이해하기 위해 주석을 꼼꼼히 달아놨다.
// 로그인 요청마다 임의의 문자열 'nonce' 생성
// 'nonce'는 앱의 인증 요청에 대한 응답 -> ID 토큰이 명시적으로 부여되었는지 확인하는 데 사용
// 재전송 공격을 방지하기 위한 함수
private func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
var randomBytes = [UInt8](repeating: 0, count: length)
let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes)
if errorCode != errSecSuccess {
fatalError(
"Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)"
)
}
let charset: [Character] =
Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
let nonce = randomBytes.map { byte in
// Pick a random character from the set, wrapping around if needed.
charset[Int(byte) % charset.count]
}
return String(nonce)
}
// 'nonce'의 SHA256 해시를 전송하면 Apple은 이에 대한 응답으로 원래의 값 전달
// Firebase는 원래의 nonce를 해싱하고 Apple에서 전달한 값과 비교하여 응답을 검증
@available(iOS 13, *)
private 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
}
// Apple의 응답을 처리하는 대리자 클래스와 nonce의 SHA256 해시를 요청에 포함하는 것으로 Apple의 로그인 과정 시작
@available(iOS 13, *)
private func startSignInWithAppleFlow() {
let nonce = randomNonceString()
currentNonce = nonce
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(nonce)
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
Apple 로그인의 nonce를 사용하는데 nonce란 주로 보안 및 암호화 분야에서 사용되며, 암호화 프로세스에서 중요한 역할을 한다. 애플ID 인증값을 요청할 때, request하여 생성 후 전달하는 방식인데, 이 때 nonce 값을 포함하여 전달하게 된다. 결론은, 이러한 과정이 포함되어 있지 않으면, 애플로그인 기능을 이용할 수 없다.
이제 로그인과 Firebase 로그인을 연동하는 기능을 구축해준다. ASAuthorizationControllerDelegate을 이용한다.
// delegate를 구현하여 Apple의 응답을 처리.
// 로그인에 성공했으면 해시되지 않는 nonce가 포함된 Apple의 응답에서 ID 토큰을 이용하여 Firebase에 인증
@available(iOS 13.0, *)
extension LoginVC : ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return self.view.window!
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
guard let nonce = currentNonce else {
fatalError("Invalid state: A login callback was received, but no login request was sent.")
}
// identityToken 가져오기
guard let appleIDToken = appleIDCredential.identityToken else {
print("Unable to fetch identity token")
return
}
// 가져온 identityToken, String 타입 변환
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
return
}
// 변환한 identityToken을 Firebase 로그인 인증에 맞게 할당
let credential = OAuthProvider.credential(withProviderID: "apple.com",
idToken: idTokenString,
rawNonce: nonce)
// Firebase에 로그인
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
// Error. If error.code == .MissingOrInvalidNonce, make sure
// you're sending the SHA256-hashed nonce as a hex string with
// your request to Apple.
print(error.localizedDescription)
return
}
print("identityToken: \(idTokenString)")
if let email = appleIDCredential.email {
print("Email: \(email)")
} else {
print("Email not provided")
}
if let fullName = appleIDCredential.fullName {
let displayName = "\(fullName.givenName ?? "") \(fullName.familyName ?? "")"
print("Full Name: \(displayName)")
} else {
print("Full Name not provided")
}
self.dismiss(animated: true, completion: nil)
// Apple 로그인을 통한 Firebase 로그인 성공 & SettingVC로 자동 전환
}
// 사용자의 authorizationCode를 로그인 시 미리 가져온다. 회원 탈퇴 시, 필요하기 때문이다.
if let authorizationCode = appleIDCredential.authorizationCode, let codeString = String(data: authorizationCode, encoding: .utf8) {
let url = URL(string: "https://us-central1-everydiary-a9c5e.cloudfunctions.net/getRefreshToken?code=\(codeString)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "https://apple.com")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
if let data = data {
let refreshToken = String(data: data, encoding: .utf8) ?? ""
print(refreshToken)
UserDefaults.standard.set(refreshToken, forKey: "refreshToken")
UserDefaults.standard.synchronize()
}
}
task.resume()
}
}
}
// 로그인이 제대로 되지 않았을 경우, Error 발생
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
// Handle error.
print("로그인 실패 - \(error.localizedDescription)")
}
}
이후 애플 소셜 로그인 버튼을 만들어준다. 주의해야 할 점은 Apple이 정해놓은 규칙에 어긋나는 버튼을 만들어 놓을 경우, 앱 심사에서 reject이 된다고 한다.
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple
이런 것까지 정해놓은 것 보면, 애플 종사자들은 변태에 가까운 것 같다.
버튼을 구성하고 #selector
역할에 미리 만들어놓은 함수인 startSignInWithAppleFlow()
를 입력하면 완성이다.