Sign in with Apple

Introduction

Trong sự kiện WWDC 2019, Apple đã giới thiệu một login service mới tập trung vào sự bảo mật và tính riêng tư, được gọi là Sign in with Apple. Tại thời điểm đó, Apple có nói rằng nếu một app iOS sử dụng third-party login service thì bắt buộc sẽ phải sử dụng thêm Sign in with Apple.

Ngày 12-09-2019, guildline chính thức về việc sử dụng Sign in with Apple đã được đưa ra.

https://developer.apple.com/app-store/review/guidelines/#sign-in-with-apple

Qua đó:

Các app mới submit lên App Store cũng như các app hiện tại và các bản update bắt buộc phải tuân thủ guildline Sign in with Apple. Áp dụng kể từ tháng 4 năm 2020.

Các app đang sử dụng các login service của các third-party, mạng xã hội (như Facebook Login, Google Sign-In, Sign in with Twitter, Sign In with LinkedIn, Login with Amazon hoặc WeChat Login) để xác thực tài khoản người dùng thì phải cung cấp thêm lựa chọn tương đương Sign in with Apple. Tài khoản người dùng (user's primary account) được hiểu là tài khoản được tạo để xác thực, đăng nhập và truy cập các tính năng và các dịch vụ.

Không cần implement Sign in with Apple trong các trường hợp sau:

  • Những app sử dụng hệ thống đăng nhập internal riêng của một công ty, tập đoàn để đăng nhập.
  • Những app thuộc lĩnh vực giáo dục, doanh nghiệp mà yêu cầu người dùng đăng nhập bằng một tài khoản giáo dục, doanh nghiệp đã tồn tại sẵn.
  • Những app được hỗ trợ bởi chính phủ, tổ chức sử dụng hệ thống xác thực công dân để xác minh bằng các hệ thống quản lý công dân, căn cước, ID điện tử
  • Những app là client dịch vụ cho một bên thứ 3 cụ thể, yêu cầu người dùng đăng nhập để có thể truy cập, sử dụng content, media của bên thứ 3 đó.

Cuối cùng thì không quan trọng là app cũ hay mới, chúng ta cũng cần thêm tính năng Sign in with Apple, vậy thì hãy làm quen với nó thôi. Trong bài viết phần 1 này, chúng ta sẽ chỉ tập trung vào phần thêm option Sign in with Apple vào app.

Why Sign in with Apple?

Sign in with Apple là cách thức giúp người dùng có thể đăng ký, đăng nhập tài khoản và sử dụng app, website của bạn. Apple hứa hẹn rằng đây là một login service thân thiện, riêng tư, nhanh và bảo mật. Hãy cùng chờ xem điều đó có đúng hay không vì thời gian sẽ trả lời tất cả.

Capability

Để implement tính năng này, điều đầu tiên chúng ta cần làm đó là thêm mới capability.

Bằng cách click vào app target > Signing & Capabilities tab > click + Capability > double click vào "Sign in with Apple".

Login Button

Apple rất khôn ngoan trong việc support developer tận răng, tạo sẵn cả một button class ASAuthorizationAppleIDButton có UI sẵn cho việc Sign in with Apple.

Để tạo mới login button, chúng ta chỉ cần import framework AuthenticationServices rồi init và sử dụng nó như một UIButton bình thường, không có gì đặc biệt cả.

let authorizationButton = ASAuthorizationAppleIDButton()
authorizationButton.center = view.center
view.addSubview(authorizationButton)

Button Size and Position

UI của login button này của Apple mặc định không thể thay đổi. Chúng ta không thể thay đổi logo quả táo, font hay text size, chỉ có thể thay đổi frame. Font size của button sẽ tự thay đổi theo.

Sign in with Apple button với các frame size khác nhau: 130x30, 200x30, 260x30, 130x50, 130x100, 200x100, 260x100, 300x100

Như chúng ta có thể thấy, UI của button không thể can thiệp được. Vì vậy để đồng bộ UI với các login button của các login service khác, hãy layout login button của Apple trước, từ đó xác định font size phù hợp cho các login button khác.

Light and Dark Mode

Mặc định, login button của Apple sẽ không tự động adapt tùy theo Light hay Dark Mode. Vì vậy chúng ta cần tự handle việc này:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    
    let hasUserInterfaceStyleChanged = traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection)
    
    if hasUserInterfaceStyleChanged {
        setupProviderLoginView()
    }
}

func setupProviderLoginView() {
    switch traitCollection.userInterfaceStyle {
    case .dark:
        authorizationButton = ASAuthorizationAppleIDButton(type: .default, style: .white)
    default:
        authorizationButton = ASAuthorizationAppleIDButton(type: .default, style: .black)
    }
    ...
}

Ngoài 2 style .white.black, chúng ta còn có thể sử dụng style .whiteOutline.

Corner radius

Điều cuối cùng mà chúng ta có thể customize Apple login button đó là corner radius. Hay thay đổi sao cho đồng nhất với corner radius của các login button khác.

authorizationButton.cornerRadius = 0

Action

Sau khi setup trong appearance của Sign in with Apple button, chạy thử app, tap vào nhưng sẽ không có action nào xảy ra cả. Đơn giản vì ASAuthorizationAppleIDButton chỉ là một view bình thường, không chứa logic, chúng ta vẫn phải thêm action handler cho nó như một UIButton bình thường.

authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)

Apple đã nhiệt tình support bằng việc làm hết các việc khó cho developer khi cung cấp ASAuthorizationController - một view controller sẽ handle mọi presentation logic và UI cần thiết cho việc đăng nhập.

Điều duy nhất mà chúng ta cần phải làm là set các thông tin cần thiết thông qua ASAuthorizationAppleIDRequest từ ASAuthorizationAppleIDProvider và truyền vào parameter của ASAuthorizationController.

Trong ví dụ dưới đây, chúng ta sẽ request emailfullName.

@objc
private func handleAuthorizationAppleIDButtonPress() {
    // Tạo request từ ASAuthorizationAppleIDProvider
    let appleIDProvider = ASAuthorizationAppleIDProvider()
    let request = appleIDProvider.createRequest()
    // Set các field cần get
    request.requestedScopes = [.fullName, .email]
    
    // Present authorizationController và perform request vừa tạo
    let authorizationController = ASAuthorizationController(authorizationRequests: [request])
    authorizationController.delegate = self
    authorizationController.presentationContextProvider = self
    authorizationController.performRequests()
}

presentationContextProvider (ASAuthorizationControllerPresentationContextProviding) yêu cầu chỉ định một window annchor để present login dialog.

// MARK: - ASAuthorizationControllerPresentationContextProviding
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
    return self.view.window!
}

delegate (ASAuthorizationControllerDelegate) có 2 method callback khi sign-in request thành công hoặc thất bại.

// MARK: - ASAuthorizationControllerDelegate
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {        
        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
        }
                
        let userIdentifier = appleIDCredential.user
        let fullName = appleIDCredential.fullName
        let email = appleIDCredential.email
        ...
    }
}

func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
    // Handle error.    
    guard let error = error as? ASAuthorizationError else {
        return
    }

    switch error.code {
    case .canceled:
        print("Canceled")
    case .unknown:
        print("Unknown")
    case .invalidResponse:
        print("Invalid Respone")
    case .notHandled:
        print("Not handled")
    case .failed:
        print("Failed")
    @unknown default:
        print("Default")
    }
}

Trên đây là tất cả những gì cần làm để tích hợp tính năng Sign in with Apple vào app của bạn. Build lại app và tap vào button và xem action xảy ra.

Sign in cases

Flow sign-in khá đơn giản, hãy xem các trường hợp có thể xảy ra khi tap vào button này.

No Apple ID

Nếu như device của bạn không có Apple ID thì delegate sẽ call method authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) với error trả về ASAuthorizationErrorUnknown.

Với trường hợp này, chúng ta không cần làm gì hết, iOS sẽ tự hiển thị alert thông báo và hướng dẫn người dùng đăng nhập Apple ID.

Để đăng nhập vào một app hoặc website sử dụng tính năng Sign in with Apple, chúng ta cần có:

  • Một Apple ID có bật xác thực 2 bước.
  • Đăng nhập iCloud sử dụng Apple ID đó trên device của Apple.

Chi tiết xem How to use Sign in with Apple.

First time

Nếu người dùng đã đăng nhập Apple ID, khi request lần đầu, màn hình Data and privacy information sẽ được hiển thị:

Nếu người dùng tap vào button Cancel thì error return sẽ là ASAuthorizationErrorCanceled.

Còn nếu tap vào Continue thì hệ thống sẽ chuyển đến giao diện đăng nhập tiếp theo.

Trên simulator, mỗi lần tap vào buttin Sign in with Apple đều sẽ luôn được tính là lần đầu tiên. Vậy nên chúng ta sẽ thấy giao diện này hiển thị liên tục.

Information granted

Ở màn hình đăng nhập tiếp theo, hệ thống sẽ hiển thị một form điền trước thông tin đăng nhập được đăng ký trong Apple ID đó.

Đó có thể là email full name hoặc cả hai, tùy thuộc vào requestedScopes được chỉ định trong request ban đầu.

let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]

Người dùng có thể tap vào form này và edit các thông tin tùy ý.

Đối với email, ngoài email chính, người dùng còn có thể chọn bất kỳ email nào liên kết đến tài khoản Apple đó.

Hoặc người dùng có thể ẩn email của mình bằng việc chọn option Hide my email. Với option này, Apple sẽ tạo một địa chỉ email nặc danh (anonymous) cho người dùng (một email có dạng một chuỗi random với đuôi @privaterelay.appleid.com).

Các email anonymous này (private relay email addresses) có một số đặc điểm sau:

  • Có đuôi @privaterelay.appleid.com
  • Các email gửi đến sẽ được chuyển hướng đến một trong các email đã được xác thực của Apple ID đó (một kiểu không public địa chỉ email nhưng bạn vẫn nhận được các email khi cần thiết).
  • Các app của cùng một development team sẽ dùng chung một email private này. App của development team khác sẽ có một email private khác.
  • Các email này luôn được active, bạn có thể truy cập bất kỳ lúc nào để gửi, nhận email, ngay cả khi app đó active hay không active, được cài hay đã gỡ bỏ.

Sau khi share một private relay email với app, người dùng có thể tìm, xem lại và quản lý chúng bằng cách truy cập vào.

Settings > Apple ID > Password & Security > Apple ID Logins

Tất cả các anonymous email được cấp sẽ được lưu trữ ở đây.

Done

Sau khi đăng nhập hoàn tất, delegate sẽ call method authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) với return object authorization: ASAuthorization chứa các thông tin granted.

Chúng ta có thể lấy ra các thông tin về fullName, email, useridentityToken từ object này.

func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {        
        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
        }
        
        
        let userIdentifier = appleIDCredential.user
        let fullName = appleIDCredential.fullName
        let email = appleIDCredential.email
        
        print(idTokenString)
        print(userIdentifier)
        print(fullName)
        print(email)  
    } 
}

Trên simulator, chúng ta có thể test tính năng Sign in with Apple bao nhiêu lần cũng được. Các thông tin lấy được (full name, email, user và identity token) luôn được return trong method authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization).

Nhưng trên device thật sẽ không như vậy.

Một khi đã grant các thông tin đăng nhập thì iOS sẽ hiển thị thông báo này với người dùng.

Tap vào button Continue sẽ vẫn call delegate method success nhưng full name và email sẽ không được trả về trong object ASAuthorization nữa.

Chúng ta chỉ có thể nhận được thông tin đăng nhập ở lần đăng nhập đầu tiên. Vì vậy hãy sử dụng ngay các thông tin full name, email để thực hiện quá trình đăng ký/đăng nhập với hệ thống của bạn.

Cẩn thận hơn, hãy lưu và bảo mật các thông tin này trong quá trình xác thực người dùng và xóa đi khi quá trình này kết thúc.

Behavior chỉ có thể get email, full name ở lần đầu tiên Sign in with Apple đã được Patrick - nhân viên của Apple xác nhận ở đây:

https://forums.developer.apple.com/thread/121496#379297

Vậy nên code trong method authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) cần chú ý như sau:

func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {        
        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
        }
        
        
        let userIdentifier = appleIDCredential.user
        let fullName = appleIDCredential.fullName
        let email = appleIDCredential.email
        
        // Lưu và bảo mật email, full name vì không thể get lại ở lần sau. Ví dụ: lưu vào keychain.
        // Thực hiện quá trình đăng nhập/đăng ký, xác thực người dùng ở hệ thống của bạn.
        // Nếu quá trình xác thực người dùng thành công, remove các thông tin đăng nhập này trong keychain.
        // Lưu accessToken nhận được từ hệ thống của bạn.
        // Còn nếu xác thực thất bại, lấy lại các thông tin email, full name và retry.
    } 
}

Source article


All Rights Reserved