|
| 1 | +<!-- markdownlint-disable MD002 MD041 --> |
| 2 | + |
| 3 | +<%= include('../../_includes/_getting_started', { library: 'Swift' }) %> |
| 4 | + |
| 5 | +Add your credentials in the `Auth0.plist` file. If the file does not exist in your project yet, create it: |
| 6 | + |
| 7 | +```xml |
| 8 | +<!-- Auth0.plist --> |
| 9 | + |
| 10 | +<?xml version="1.0" encoding="UTF-8"?> |
| 11 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 12 | +<plist version="1.0"> |
| 13 | +<dict> |
| 14 | + <key>ClientId</key> |
| 15 | + <string>${account.clientId}</string> |
| 16 | + <key>Domain</key> |
| 17 | + <string>${account.namespace}</string> |
| 18 | +</dict> |
| 19 | +</plist> |
| 20 | +``` |
| 21 | + |
| 22 | +<%= include('../../_includes/_ios_dependency_centralized') %> |
| 23 | + |
| 24 | +## Add the "Sign In With Apple" Button |
| 25 | + |
| 26 | +As an example of adding the sign-in button to your view, add a `UIStackView` to your main storyboard and create an outlet in `ViewController.swift`: |
| 27 | + |
| 28 | +```swift |
| 29 | + @IBOutlet weak var loginProviderStackView: UIStackView! |
| 30 | +``` |
| 31 | + |
| 32 | +Next, add the `AuthenticationServices` Framework to your `ViewController.swift`: |
| 33 | + |
| 34 | +```swift |
| 35 | +import AuthenticationServices |
| 36 | +``` |
| 37 | + |
| 38 | +Add a function that attaches an `ASAuthorizationAppleIDButton` button to the stack view: |
| 39 | + |
| 40 | +```swift |
| 41 | +func setupProviderLoginView() { |
| 42 | + // Create Button |
| 43 | + let authorizationButton = ASAuthorizationAppleIDButton() |
| 44 | + |
| 45 | + // Add Callback on Touch |
| 46 | + authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside) |
| 47 | + |
| 48 | + //Add button to the UIStackView |
| 49 | + self.loginProviderStackView.addArrangedSubview(authorizationButton) |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +Call this function in `viewDidLoad`: |
| 54 | + |
| 55 | +```swift |
| 56 | +override func viewDidLoad() { |
| 57 | + super.viewDidLoad() |
| 58 | + |
| 59 | + setupProviderLoginView() |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +## Handle Authorization |
| 64 | + |
| 65 | +The following code shows the implementation of a button handler that initializes an authorization request and launches |
| 66 | +the native Sign In With Apple flow: |
| 67 | + |
| 68 | +```swift |
| 69 | +@objc |
| 70 | +func handleAuthorizationAppleIDButtonPress() { |
| 71 | + // Create the authorization request |
| 72 | + let request = ASAuthorizationAppleIDProvider().createRequest() |
| 73 | + |
| 74 | + // Set scopes |
| 75 | + request.requestedScopes = [.email, .fullName] |
| 76 | + |
| 77 | + // Setup a controller to display the authorization flow |
| 78 | + let controller = ASAuthorizationController(authorizationRequests: [request]) |
| 79 | + |
| 80 | + // Set delegates to handle the flow response. |
| 81 | + controller.delegate = self |
| 82 | + controller.presentationContextProvider = self |
| 83 | + |
| 84 | + // Action |
| 85 | + controller.performRequests() |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +:::note |
| 90 | +At this time the only scopes that Apple support are `email` and `fullName`. For more information, see [ASAuthorization.scope](https://developer.apple.com/documentation/authenticationservices/asauthorization/scope). |
| 91 | +::: |
| 92 | + |
| 93 | +At this point, you will have a few compile errors to deal with. Before moving forward with all the code, let's remove those errors by providing some basic delegates. |
| 94 | + |
| 95 | +### Add authorization extensions |
| 96 | + |
| 97 | +Add the following to the end of the `ViewController.swift` file. This enables the authorization flow UI to be displayed from the controller: |
| 98 | + |
| 99 | +```swift |
| 100 | +extension ViewController: ASAuthorizationControllerPresentationContextProviding { |
| 101 | + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { |
| 102 | + return self.view.window! |
| 103 | + } |
| 104 | +} |
| 105 | +``` |
| 106 | + |
| 107 | +Next, add a stub delegate for the `ASAuthorizationController` to the end of the `ViewController.swift` file. This code will handle the response sent to the `ASAuthorizationController`. You will come back to this one later. |
| 108 | + |
| 109 | +```swift |
| 110 | +extension ViewController: ASAuthorizationControllerDelegate { |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +### Add the Sign In With Apple capability |
| 115 | + |
| 116 | +To enable Sign In With Apple in your application, you need to enable this capability to your app, as shown: |
| 117 | + |
| 118 | + |
| 119 | + |
| 120 | +:::panel Checkpoint |
| 121 | +Start the application and click the **Sign In With Apple** button. You should see the default sign-in dialog appear: |
| 122 | + |
| 123 | + |
| 124 | +::: |
| 125 | + |
| 126 | +## Process the Authorization Response |
| 127 | + |
| 128 | +Add the following to the end of your `ViewController.swift` file. |
| 129 | + |
| 130 | +```swift |
| 131 | +extension ViewController: ASAuthorizationControllerDelegate { |
| 132 | + // Handle authorization success |
| 133 | + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { |
| 134 | + |
| 135 | + if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { |
| 136 | + // Success |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + // Handle authorization failure |
| 141 | + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { |
| 142 | + print("Authorization Failed: \(error)") |
| 143 | + } |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +### Inspect the response |
| 148 | + |
| 149 | +Upon successful authorization, you will be able to access the following information: |
| 150 | + |
| 151 | +- UUID, team-scoped user ID |
| 152 | +- Verification Data |
| 153 | + - ID Token, Authorization Code |
| 154 | +- Account Information |
| 155 | + - Name, Verified Email Address (Original or relay address) |
| 156 | +- Real user indicator |
| 157 | + - High confidence indicator that the user is who they say they are |
| 158 | + |
| 159 | +:::note |
| 160 | +The Account Information is ONLY sent on the first successful authorization. This information should be saved by your app if you wish to use it when a user leaves and reloads your application. |
| 161 | +::: |
| 162 | + |
| 163 | +## Auth0 Token Exchange |
| 164 | + |
| 165 | +Now that you have a successful authorization response, you can use the `authorizationCode` to perform a token exchange. The token exchange will give you access to Auth0 credentials, such as the ID and Access Tokens. For example: |
| 166 | + |
| 167 | +```swift |
| 168 | +if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { |
| 169 | + // Convert Data -> String |
| 170 | + guard let authorizationCode = appleIDCredential.authorizationCode, let authCode = String(data: authorizationCode, encoding: .utf8) else |
| 171 | + { |
| 172 | + print("Problem with the authorizationCode") |
| 173 | + return |
| 174 | + } |
| 175 | + |
| 176 | + // Auth0 Token Exchange |
| 177 | + Auth0 |
| 178 | + .authentication() |
| 179 | + .tokenExchange(withAppleAuthorizationCode: authCode).start { result in |
| 180 | + switch(result) { |
| 181 | + case .success(let credentials): |
| 182 | + print("Auth0 Success: \(credentials)") |
| 183 | + |
| 184 | + case .failure(let error): |
| 185 | + print("Exchange Failed: \(error)") |
| 186 | + } |
| 187 | + |
| 188 | + } |
| 189 | +} |
| 190 | +``` |
| 191 | + |
| 192 | +## Renew Authentication |
| 193 | + |
| 194 | +When users return to your app, you can log them in automatically if their login session is still valid. This involves: |
| 195 | + |
| 196 | +* Calling `ASAuthorizationAppleIDProvider.getCredentialState` to make sure the user is still authorized |
| 197 | +* Retrieving Auth0 credentials |
| 198 | + |
| 199 | +To do this, you're going to: |
| 200 | + |
| 201 | +* Use `CredentialsManager` to store the Auth0 credentials |
| 202 | +* Use `SimpleKeychain` to store the Apple user ID |
| 203 | +* Check the credential state using Apple's APIs before retrieving Auth0 credentials |
| 204 | + |
| 205 | +### Store credentials |
| 206 | + |
| 207 | +The credentials manager retrieves stored credentials from the keychain and checks if the Access Token is still valid: |
| 208 | + |
| 209 | +* If the current credentials are still valid, the credentials manager returns them |
| 210 | +* If the Access Token has expired, the credentials manager renews them using the Refresh Token and returns them |
| 211 | + |
| 212 | +At the top of `ViewController.swift`, add a `CredentialsManager` and `SimpleKeychain` as follows: |
| 213 | + |
| 214 | +```swift |
| 215 | +let credentialsManager = CredentialsManager(authentication: Auth0.authentication()) |
| 216 | +let keychain = A0SimpleKeychain() |
| 217 | + |
| 218 | +// Add a place to store credentials locally once they're renewed |
| 219 | +var credentials: Credentials? |
| 220 | +``` |
| 221 | + |
| 222 | +Next, modify the token exchange call you added earlier to store credentials when the user logs in: |
| 223 | + |
| 224 | +```swift |
| 225 | +Auth0 |
| 226 | + .authentication() |
| 227 | + .tokenExchange(withAppleAuthorizationCode: authCode).start { result in |
| 228 | + switch(result) { |
| 229 | + case .success(let credentials): |
| 230 | + |
| 231 | + // NEW - store the user ID in the keychain |
| 232 | + self.keychain.setString(appleIDCredential.user, forKey: "userId") |
| 233 | + |
| 234 | + // NEW - store the credentials locally |
| 235 | + self.credentials = credentials |
| 236 | + |
| 237 | + // NEW - store the credentials in the credentials manager |
| 238 | + self.credentialsManager.store(credentials: credentials) |
| 239 | + |
| 240 | + case .failure(let error): |
| 241 | + print("Exchange Failed: \(error)") |
| 242 | + } |
| 243 | +} |
| 244 | +``` |
| 245 | + |
| 246 | +### Renew authentication state |
| 247 | + |
| 248 | +Add a function that tries to renew the user's login session. Here, `getCredentialState` is called to find out whether the user is still authorized for Sign In With Apple. If they are **unauthorized**, you should consider their access revoked, their access and refresh tokens should be removed, and the user should log into the application again. Otherwise, the credentials that were previously stored inside `CredentialsManager` can be returned, as in the following example: |
| 249 | + |
| 250 | +```swift |
| 251 | +func tryRenewAuth(_ callback: @escaping (Credentials?, Error?) -> ()) { |
| 252 | + let provider = ASAuthorizationAppleIDProvider() |
| 253 | + |
| 254 | + // Try to fetch the user ID |
| 255 | + guard let userID = keychain.string(forKey: "userId") else { |
| 256 | + return callback(nil, nil) |
| 257 | + } |
| 258 | + |
| 259 | + // Check the Apple credential state |
| 260 | + provider.getCredentialState(forUserID: userID) { state, error in |
| 261 | + switch state { |
| 262 | + case .authorized: |
| 263 | + // Try to get credentials from the creds manager (ID token, Access Token, etc) |
| 264 | + self.credentialsManager.credentials { error, credentials in |
| 265 | + guard error == nil, let credentials = credentials else { |
| 266 | + return callback(nil, error) |
| 267 | + } |
| 268 | + |
| 269 | + self.credentials = credentials |
| 270 | + |
| 271 | + callback(credentials, nil) |
| 272 | + } |
| 273 | + |
| 274 | + default: |
| 275 | + // User is not authorized |
| 276 | + self.keychain.deleteEntry(forKey: "userId") |
| 277 | + |
| 278 | + // Remove their credentials from the store |
| 279 | + self.credentialsManager.clear() |
| 280 | + |
| 281 | + callback(nil, error) |
| 282 | + } |
| 283 | + } |
| 284 | +} |
| 285 | +``` |
| 286 | + |
| 287 | +:::note |
| 288 | +Calling `credentialsManager.credentials` _automatically renews_ the Access Token if it has expired, using the refresh token. This call should only be executed if `getCredentialState` returns `authorized`, so that the refresh token is only used by an authorized user. Otherwise, the credentials must be cleared and the login session thrown away. |
| 289 | +::: |
| 290 | + |
| 291 | +Finally, call this function from `viewDidLoad`. If no credentials are found, the user should be shown the login screen once more. Otherwise, they should continue on into the app: |
| 292 | + |
| 293 | +```swift |
| 294 | +tryRenewAuth { credentials, error in |
| 295 | + guard error == nil, credentials != nil else { |
| 296 | + print("Unable to renew auth: \(String(describing: error))") |
| 297 | + |
| 298 | + // The user should be asked to log in again |
| 299 | + |
| 300 | + return |
| 301 | + } |
| 302 | + |
| 303 | + // Set up any post-login UI or segue here |
| 304 | +} |
| 305 | +``` |
| 306 | + |
| 307 | +## Next Steps |
| 308 | + |
| 309 | +Now that you're able to use Sign In With Apple and exchange the authorization code for Auth0 credentials, you can use these credentials to perform various tasks: |
| 310 | + |
| 311 | +* Use the Access Token to [Call APIs](/quickstart/native/ios-swift/04-calling-apis) |
| 312 | +* [Assign roles to users](/quickstart/native/ios-swift/05-authorization) |
| 313 | +* Use the Access Token to [link accounts](/quickstart/native/ios-swift/07-linking-accounts) |
0 commit comments