EveryDiary - 로그인 서비스 (2) Apple

ulls12·2024년 3월 6일
0

Swift TIL

목록 보기
49/60

초기 세팅

  1. Apple 개발자 등록
    Apple 로그인을 구현하기에 앞서 개발자 사이트에 가입을 해야한다. 개발자 등록을 하기 위한 연회비는 129,000원이다.
  2. Signing 추가
  • Capability 를 누르고 Sign in with Apple을 검색하고 추가하면 아래와 같이 나온다.
  1. Firebase Console에 Apple login 추가
    새 제공업체 추가를 누르고 Apple을 클릭한다.



4. App ID 등록
Apple 개발자 페이지로 이동하여 인증서, 식별자 및 프로파일을 클릭하여 이동한다.

Identifiers 에서 + 버튼을 누르고 AppID 등록을 한다.
Description에는 App 이름을 적고, Bundle ID는 Xcode에서 프로젝트 파일에서 볼 수 있는 Bundle ID와 일치하게 적어주면 된다.
아래의 Capabilities를 살펴보면, 항목중에 Sign in with Apple을 찾아서 체크박스를 체크해준다.
모두 작성하고 Continue 를 누르고 Register를 한 번 더 누르면 AppID가 생성된다.

  1. Service ID 등록
    그후 다시 Identifiers 탭에서 +버튼을 다시 누른다.

    이번엔 Service ID를 선택하고 Continue를 누른다

    Description에는 FirebaseLogin 기능을 뜻하는 이름을 지어주고, com.domainname.appname을 되도록 지키는 선으로 ID를 지어준다.
    Continue를 누르고 Register를 한번 더 눌러 등록한다.

  2. 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()를 입력하면 완성이다.

profile
I am 개발해요

0개의 댓글