본문 바로가기

iOS

[iOS] FirebaseAuth를 통해 OAuth 2.0 소셜로그인 기능 구현하기

로그인이란?

 로그인 기능은 사용자의 신원 정보를 필요로 하는 어플리케이션에서 필수적인 과정이다.

로그인을 통해서 사용자가 생성한 데이터를 사용자 별로 안전하게 저장할 수 있고, 다른 환경에서도 사용자 특화된 레이아웃이나 데이터를 제공할 수 있다.

 

 그리고 소셜로그인이란 SNS에서 제공하는 로그인 기능을 통해서 다른 웹사이트 또는 앱에 로그인하는 것을 말하며, 로그인 방식의 하나인 동시에 OAuth 2.0을 통한 인증 방식 중의 하나이다. 소셜로그인을 이용하면 해당 서비스에 계정을 새로 만들 필요 없이 기존에 OAuth를 사용하는 서비스(구글, 카카오, 애플)의 인증 정보에 대한 권한을 가져와서 사용할 수 있다.

 

 iOS 앱에 대해서는 애플이 제공하는 소셜로그인 가이드가 존재한다. 그 중 하나가 특정 앱에서 소셜로그인을 제공한다면, 애플로그인도 필수적으로 제공해야 한다는 것이다. 만약 구글로그인만 구현된 상태라면 애플의 가이드라인에 위배되는 것이다. 따라서 iOS 앱에서 소셜로그인을 구현한다면 애플로그인을 필수적으로 구현해야 한다.

OAuth란?

 위에서 언급한 OAuth란 사용자 인증을 위한 국제 표준이다. ID와 PW 같은 사용자의 개인정보를 노출하지 않으면서, OAuth를 사용하는 업체의 API 접근 권한을 안전하게 위임 받기 위해 사용한다.

 

 기본적인 개념은 다음과 같다.

User : Service Provider에 계정을 가지고 있는 사용자
Consumer : Service Provider의 API를 사용하려는 서비스(앱, 웹)
Service Provider : OAuth를 사용하여 API를 제공하는 서비스(구글, 페이스북, 카카오 등)
Access Token : 인증 완료 후 Service Provider의 제공 기능을 이용할 수 있는 권한을 위임받은 인증 키

 

 로그인 기능에서는, 사용자가 입력한 ID와 패스워드가 Client를 통해 입력되면 Server에서는 이것이 맞는지 판단하고 인증키를 제공한다.

Firebase를 이용한 소셜로그인 기능 구현

Firebase란?

 iOS, Android 앱과 같은 모바일 및 웹 어플리케이션 개발 플랫폼이다. Backend가 필요한 많은 기능들을 Serverless로 개발할 수 있도록 도와주는 플랫폼이다. 소규모 웹 서비스, 모바일 앱, 1인 개발자, 초심자를 위한 서비스라고 생각할 수 있다.

FirebaseAuth

 개인 개발자나 초창기 스타트업과 같이 별도의 백엔드 서버가 없는 경우 편하게 이용할 수 있는 Firebase의 인증 서비스이다.

플로우는 다음과 같다.

  1. User는 소셜 로그인(Apple) 요청을 한다.
  2. 그러면 이 앱이 Service Provider(Apple)에 권한을 위임해 달라고 유저에게 요청한다.
  3. Service Provider는 유저에게 권한 위임에 대한 의사를 유저에게 묻는다.
  4. 권한 이임을 확인하면 Service Provider는 사용자의 앱에 사용자의 일부 정보(권한을 위임받은), 즉 이메일이나 이름과 같은 정보를 사용할 수 있는 인증키를 제공한다.
  5. 앱에서는 인증키를 기반으로 권한이 필요한 기능을 사용할 수 있다.

 이러한 토큰은 원래 백엔드 서버에서 관리하게 되는데, Serverless에서 개발을 하기 위해 FirebaseAuth 플랫폼에서 대신 관리를 해주는 것이다.

 Firebase 인증을 제공하는 업체는 이메일/비밀번호, 구글, 깃허브, 마이크로소프트, 애플, 익명 등이 있지만 한국에서 중요한 카카오나 네이버를 통한 인증을 제공하지 않는다. 이러한 경우 Firebase에서 제공하는 SignInWithCustomToken 기능을 이용할 수 있다.

Firebase의 유저 인증 기능

Firebase의 유저 객체
 Firebase에는 User 객체가 존재하는데, Firebase 프로젝트의 앱에 가입한 사용자 계정을 말한다. 만약 프로젝트 내에 여러 앱이 존재한다면 이 앱들이 User 객체를 공유할 수 있다. 따라서 하나의 프로젝트에 등록된 앱들에 대한 유저를 동시에 관리하고, 정보를 연동할 수 있게 된다.

이러한 유저 객체에는 고유 ID, 이메일 주소, 이름, 사진 URL과 같은 고유한 기본 속성들을 가지는데, 클라이언트에서 원한다면 이러한 정보를 업데이트할 수 있고, 앱 내에서 접근하여 사용할 수 있다.

 

Firebase의 Auth 객체
 Firebase를 통해서 로그인하거나 회원가입한 User는, Auth 인스턴스의 현재 사용자(Current User)가 된다. 이 Auth 인스턴스는 앱을 재시작해도 사용자에 대한 참조를 유지하기 때문에 사용자의 정보가 사라지지 않게 한다.

현재 사용자가 로그아웃하게 되면 Auth 인스턴스는 user 객체에 대한 참조를 끊기 때문에, 현재 사용자가 없어지게 된다. 하지만 로그아웃 전에 User 객체에 참조를 유지한다면 여전히 사용자 데이터에 접근하고 업데이트 할 수 있다.

아래와 같이 currentUser 속성에 접근하여 현재 사용자의 정보를 얻어올 수 있다.

 

let user = Auth.auth().currentUser
if let user = user {
  let uid = user.uid
  let email = user.email
  let photoURL = user.photoURL
}

 

만약 구글이나 애플과 같은 Firebase 제휴 업체로 로그인했다면, 아래와 같은 방식으로 사용자 정보를 얻어올 수 있다.

 

let userInfo = Auth.auth().currentUser?.providerData[indexPath.row]
cell?.textLabel?.text = userInfo?.providerID
// Provider-specific UID
cell?.detailTextLabel?.text = userInfo?.uid

 

 아래와 같이 사용자의 정보를 업데이트 할 수도 있다.

 

let changeRequest = Auth.auth().currentUser?.createProfileChangeRequest()
changeRequest?.displayName = displayName
changeRequest?.commitChanges { error in
  // ...
}

FirebaseAuth에서 다루는 세 가지 토큰


Firebase ID 토큰 : 사용자가 앱에 로그인할 때 Firebase가 만드는 JWT 토큰으로, Firebase 프로젝트 내에서 사용자의 정보를 안전하게 식별한다. 이는 Firebase 프로젝트 상에서 고유한 ID 및 사용자 프로필 정보를 함께 포함하고 있다. 이 토큰은 무결성을 보장하므로 백엔드 서버로 전송한다면, 백엔드 서버에서도 사용자의 신원을 식별할 수 있다.

 

ID 공급업체 토큰(소셜 로그인 accessToken) : Firebase와 제휴한 업체의 OAuth2.0 액세스 토큰을 말하며, 소셜 로그인 시에 공급업체로부터 토큰을 받아서 다시 Firebase에 전달하면, Firebase는 공급업체에서 정상적인 인증을 거쳤음을 확인하고 Firebase 프로젝트 내에서 사용할 수 있는 사용자 인증 정보로 변환한다.

 

Firebase 맞춤 토큰(Custom Token) : 맞춤 토큰은 Firebase와 제휴하지 않은 업체의 Oauth 2.0 액세스 토큰을 통해서 Firebase에 로그인할 때 사용할 수 있는 토큰이다.

Email과 Password로 로그인하기

 아래의 방법으로 회원가입을 진행할 수 있다. 여기서 error 객체를 분석하여 이미 존재하는 경우 SignIn 메서드를 호출해주면 로그인이 진행된다.

Auth.auth().createUser(withEmail: email, password: password) { authResult, error in
  // ...
}

 

 이메일과 패스워드를 통해 아래와 같이 로그인할 수 있다.

 

Auth.auth().signIn(withEmail: email, password: password) { [weak self] authResult, error in
  guard let strongSelf = self else { return }
  // ...
}

 

 마지막으로 아래와 같이 Firebase에서 로그아웃 할 수 있다.

 

let firebaseAuth = Auth.auth()
do {
  try firebaseAuth.signOut()
} catch let signOutError as NSError {
  print("Error signing out: %@", signOutError)
}

소셜로그인 : 제휴 업체를 통해 Firebase 로그인하기(애플을 예시로)

 아래는 Firebase 공식 문서의 코드를 그대로 인용한 것이다.

 

CryptoKit을 이용하여 nonce라는 임의의 문자열을 생성하고, nonce의 sha256 해시를 Apple에 전송하여 응답을 원래의 nonce와 비교한다. 아래를 보면 request에 nonce를 담아서 authorizationController에 파라미터로 전달하는 것을 알 수 있다. authorizationController는 애플로그인을 할 때 등장하는 로그인 Controller이다. performRequest를 시작하면 애플 로그인 인증과정이 시작된다.

import CryptoKit

// Unhashed nonce.
fileprivate var currentNonce: String?

@available(iOS 13, *)
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()
}

 

 여기서는 AsAuthorizatioNControllerDelegate를 채택하여 애플의 응답을 처리하는 부분이다. 애플의 응답으로부터 Token을 얻을 수 있고, 이를 다시 Firebase에 전달하며 SignIn하게 되는 것이다.

 

@available(iOS 13.0, *)
extension MainViewController: ASAuthorizationControllerDelegate {

  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.")
      }
      guard let appleIDToken = appleIDCredential.identityToken else {
        print("Unable to fetch identity token")
        return
      }
      guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
        print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
        return
      }
      // Initialize a Firebase credential.
      let credential = OAuthProvider.credential(withProviderID: "apple.com",
                                                IDToken: idTokenString,
                                                rawNonce: nonce)
      // Sign in with Firebase.
      Auth.auth().signIn(with: credential) { (authResult, error) in
        if 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
        }
        // User is signed in to Firebase with Apple.
        // ...
      }
    }
  }

  func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
    // Handle error.
    print("Sign in with Apple errored: \(error)")
  }

}