［SwiftUI / Auth0］Auth0 を利用した iOS アプリの開発

## この記事を書いた動機
Auth0 を利用した iOS アプリ（SwiftUI）の開発に関する記事はあるが、画面の表示を担当する `ContentView` に認証に関する処理を実装するものが多かった。

メンテナンスしやすいように認証に関する処理を別のクラスに実装したので、その備忘録として書いた。

## この記事で書くこと・書かないこと
### 書くこと
- Auth0 を利用した iOS アプリ（SwiftUI）開発

### 書かないこと
- Auth0 のアカウント作成方法
- Auth0 で新しい Application（Native App） の作成方法
- Auth0 でテストで使用するユーザーの具体的な作成方法
- Xcode を利用して新しいプロジェクト（SwiftUI）を作成する方法
- Xcode の `Add Package` を利用してライブラリを導入する方法
- プロジェクトに `.plist` ファイルを追加する方法

:::note warn
上記の内容は記載しないため、具体的な方法については他の記事を確認すること
:::

## Auth0 とは
- クラウド型の認証プラットフォーム
- ユーザのログイン情報やトークンの管理などを自社開発のアプリに導入できる


## 画面イメージ
### ログイン画面
ユーザーが **Log　in ボタン** を押すと、アプリは**認証画面**を表示する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199441/568c0a03-da0d-168e-c643-3f93833d4e14.png" width="200">

### 認証画面
ユーザーがこの画面で、メールアドレス・パスワードを入力し　**Continue　ボタン** を押すと、アプリは **Auth0** を利用して認証処理を実行する
- 認証処理が成功したら、ログイン状態になりホーム画面に遷移する

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199441/02842b66-14e7-d6d4-7cca-def0e6a8f1fa.png" width="200">


### ホーム画面
アプリは、ユーザー名とユーザーの画像を表示する
- ユーザーが **Log　out ボタン** を押すと、**Auth0** を利用してログオフ状態になりログイン画面に遷移する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199441/2b23fe6d-5414-7ae1-3e83-df173fea2a77.png" width="200">

## 前提条件
- 事前に Auth0 のアカウントを作成していること
- Xcode を利用してプロジェクト（SwiftUI）を作成していること

## 開発環境
|項目|内容|備考|
|---|---|---|
|PC|MacBook Air|M1 2020|
|IDE|Xcode|ver 14.3.1|
|バージョン管理|GitHub||

### GitHub
実際のコードを下記リポジトリにプッシュしている

:::note warn
上記 GitHub のコードには `Auth0` ファイル（`.plist` 形式） をプッシュしていないため、各自 `Auth0` のファイルを作成、設定すること
:::

https://github.com/tanakadaichi1989/Auth0

## 追加するライブラリ
- Auth0
- JWTDecode
- SimpleKeyChain

:::note info
Xcode の **Add Package** で `Auth0` を指定しインポートを実行すると、自動的に `JWTDecode` `SimpleKeyChain` も自動的に反映される。
:::

## ディレクトリ・ファイル構成
次の通り
![スクリーンショット 2023-08-03 21.45.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199441/dabd81af-a9d3-3a64-9157-7396ad5a2e5f.png)

## Auth0 で準備すること
### Application（Native App） を作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199441/67eadb38-7c66-9a57-4f9a-780d5ac479e8.png)

### 作成した Application の項目に値を設定
#### 設定対象の項目は次の通り
- **Allowed Callback URLs**
- **Allowed Logout URLs**

#### 設定する値は次の通り
```
{BundleID}://{Auth0 Domain}/ios/{BundleID}/callback
```

#### 例
|項目|値|
|---|---|
|BundleID|sample.Sample-App-with-Auth0|
|Auth0 Domain|dev-XXXXXXXXXX.us.auth0.com|

#### 設定する値
```
sample.Sample-App-with-Auth0://dev-XXXXXXXXXX.us.auth0.com/ios/sample.Sample-App-with-Auth0/callback
```

### テストで使用するユーザーを作成する
この作成したユーザーのメールアドレスとパスワードを利用して、ログインが可能。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199441/a6d93777-1068-dc87-e165-3e662e5db7cf.png)

## クラス一覧
|名称|役割|備考|
|---|---|---|
|Sample_App_with_Auth0App|エントリーポイント||
|Auth0Service|`Auth0`を利用した認証情報を各クラスに提供|`ObservableObject` を準拠する|
|ContentView|認証の状況に応じて表示する画面を切り替える||
|HomeView|ログイン後の画面||
|LoginView|ログイン前の画面||

### このクラス構成にした理由
- `Auth0` を利用した認証に関する処理は **Auth0Service クラス** に、画面に関する処理は **各 View クラス** に分けて記載したかったため

## コード
### Auth0Service クラス
```Swift:Auth0Service.swift
import Foundation
import Auth0

class Auth0Service:ObservableObject {
    @Published var isAuthenticated = false
    @Published var userProfile = Profile.empty

    internal func login() {
        Auth0
            .webAuth()
            .start { result in
                switch result {
                case .success(let credentials):
                    print("Obtained credentials: \(credentials)")
                    self.isAuthenticated = true
                    self.userProfile = Profile.from(credentials.idToken)
                case .failure(let error):
                    print("Failure: \(error.localizedDescription)")
                }
            }
    }
    
    internal func logout(){
        Auth0
            .webAuth()
            .clearSession { result in
                switch result {
                case .success:
                    print("Session cookie cleared")
                    self.isAuthenticated = false
                    self.userProfile = Profile.empty
                case .failure(let error):
                    print("Failed with: \(error.localizedDescription)")
                }
            }
    }
}
```

#### ポイント
- ログイン状態を `isAuthenticated` に保持している
- `ObservableObject` を準拠しているため `isAuthenticated` の値が変化すると、他のクラスにもそれが伝播する

### Sample_App_with_Auth0App クラス
```Swift:Sample_App_with_Auth0App.swift
import SwiftUI

@main
struct Sample_App_with_Auth0App: App {
    @StateObject var service: Auth0Service = Auth0Service()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(service)
        }
    }
}
```

#### ポイント
- `Auth0Service` クラスのインスタンスを `.environmentObject()` を利用して他のクラスが `isAuthenticated` の値を取得できるようにしている。

### ContentView クラス
```Swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @EnvironmentObject var service: Auth0Service
    var body: some View {
        if service.isAuthenticated {
            HomeView()
        } else {
            LoginView()
        }
    }
}
```
#### ポイント
- `Auth0Service` クラスの `isAuthenticated` の値に応じて表示する画面を切り替えている。

### HomeView クラス
```Swift:HomeView.swift
struct HomeView: View {
    @EnvironmentObject var service: Auth0Service
    
    var body: some View {
        VStack {
            userImage
            Text(service.userProfile.name)
                .padding()
            logoutButton
                .padding()
        }
    }
    
    private var userImage: some View {
        AsyncImage(url: URL(string: service.userProfile.picture)){ response in
            response.image?
                .resizable()
                .scaledToFit()
                .frame(width: 100,height: 100)
                .cornerRadius(50)
        }
    }
    
    private var logoutButton: some View {
        Button {
            logout()
        } label: {
            Text("Log out")
        }
        .fontWeight(.black)
        .frame(width: 160, height: 48)
        .foregroundColor(.white)
        .background(Color.primary)
        .cornerRadius(24)
    }
}


extension HomeView {
    private func logout() {
        service.logout()
    }
}
```

#### ポイント
- ボタンを押すと `logout()` を実行し、具体的な処理を `Auth0Service` クラスに委任する

### LoginView クラス
```Swift: LoginView.swift
import SwiftUI

struct LoginView: View {
    @EnvironmentObject var service: Auth0Service
    
    var body: some View {
        VStack {
            title
            loginButton
        }
    }
    
    private var title: some View {
        Text("Welcome")
            .font(.largeTitle)
            .fontWeight(.black)
    }
    
    private var loginButton: some View {
        Button {
            login()
        } label: {
            Text("Log in")
        }
        .fontWeight(.black)
        .frame(width: 160, height: 48)
        .foregroundColor(.white)
        .background(Color.primary)
        .cornerRadius(24)
    }
}


extension LoginView {
    private func login() {
        service.login()
    }
}
```
#### ポイント
- ボタンを押すと `login()` を実行し、具体的な処理を `Auth0Service` クラスに委任する

## ポイント
### ファイル Auth0 について
- ファイル形式は `.plist`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199441/fdde2cc3-40d8-0f05-810d-52ade16e0432.png)

#### Auth0 ファイルに関する設定
|項目名|データ型|値|備考|
|---|---|---|---|
|Domain|String|Auth0 の `Domain` の値||
|ClientId|String|Auth0 の `ClientId` の値||

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199441/90898224-1caf-4240-17fb-d95e89b4b5f6.png)


:::note warn
注意事項
##### Auth0 のファイル名について
- ファイル名は必ず **Auth0** にすること。
- **Auth0.plist** 等にするとエラーが発生する。


##### ClientId の項目名について
- 項目名は必ず **ClientId** にすること。
- **ClientID** や **Clientid** 等で設定するとエラーが発生する。
:::

## 参考資料
### Auth0
https://auth0.com/jp?utm_content=japanjpbrandauth0-generic-&utm_source=google&utm_campaign=apac_japan_jpn_all_ciam-all_dg-ao_auth0_search_google_text_kw_utm2&utm_medium=cpc&utm_id=aNK4z0000004IZLGA2&utm_term=auth0-c&gad=1&gclid=Cj0KCQjwoK2mBhDzARIsADGbjer4XgCB0zaRhja45IK66zPSVOKfEEshD-RAr3p1yqhxgHCqfrYfRIUaAp5qEALw_wcB

### YouTube
下記動画に手順がわかりやすく紹介されている。
英語ですが聞き取りやすいのでオススメです。

https://www.youtube.com/watch?v=X3L9GLrWQqI