【iOS】LINEログインを実装してみる

# はじめに

LINEログインの機能を簡単に触ってみたく、ドキュメント通りにやってみました。

https://developers.line.biz/ja/docs/line-login/overview/

# 環境
Xcode 13.3
Swift 5.6
LineSDKSwift 5.8.1

# 内容
### LINEディベロッパー登録

まずは登録

https://developers.line.biz/ja/services/line-login/

![スクリーンショット 2022-05-20 14.09.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/5ffb1ce0-9d44-8289-8ca4-f464cfe40d72.png)

## プロジェクト設定
https://developers.line.biz/ja/docs/ios-sdk/swift/setting-up-project/

>LINE SDK for iOS Swiftをビルドして使用するには、以下が必要です。
> - プロバイダーおよびLINEログインのチャネルを作成する。どちらもLINE Developersコンソールで作成できます。
> - デプロイメントターゲットとしてiOS 10.0以降
> - Xcode 10以降

プロバイダー、LINEログインのチャネルを作成

![スクリーンショット 2022-05-20 14.19.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/b7c85e49-6b26-95a7-8264-eb888afb43be.png)

### インストール

Cocoapodsを使っていきます

https://cocoapods.org/pods/LineSDKSwift

`pod 'LineSDKSwift', '~> 5.8.1'`でインストールしていきます

<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/70d97ea6-dc1d-9dca-cbd2-dd81c1b0c386.png">


### アプリをチャネルにリンクする
![スクリーンショット 2022-05-20 14.27.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/f99e7336-2b2f-ad4c-961e-6f1032d9e4d9.png)

### info.plistファイルを設定する

info.plistに追加

```xml
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- LINEからアプリに戻る際に利用するURLスキーマを追加 -->
            <string>line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
        </array>
    </dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
    <!-- アプリからLINEを起動する際に利用するURLスキーマを追加 -->
    <string>lineauth2</string>
</array>
```
![スクリーンショット 2022-05-20 14.32.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/3dc21f3a-54d5-6cdd-d76c-4f560fd30d86.png)

## iOSアプリにLINEログインを組み込む

https://developers.line.biz/ja/docs/ios-sdk/swift/integrate-line-login/

### `AppDelegate`と`SceneDelegate`の修正

```swift: AppDelegate
import LineSDK

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Add this to your "didFinishLaunching" delegate method.
        LoginManager.shared.setup(channelID: "YOUR_CHANNEL_ID", universalLinkURL: nil)
    
        return true
    }
}
```

```swift: SceneDelegate
import LineSDK

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        _ = LoginManager.shared.application(.shared, open: URLContexts.first?.url)
    }
}
```

### LINE SDKに組み込まれているログインボタンを使う

サンプルのままで動かしてみる

```swift
// In your view controller
override func viewDidLoad() {
    super.viewDidLoad()

    // Create Login Button.
    let loginButton = LoginButton()
    loginButton.delegate = self
    
    // Configuration for permissions and presenting.
    loginButton.permissions = [.profile]
    loginButton.presentingViewController = self
    
    // Add button to view and layout it.
    view.addSubview(loginButton)
    loginButton.translatesAutoresizingMaskIntoConstraints = false
    loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    loginButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
```

※ このままでは使えないけど👇

```swift
extension LoginViewController: LoginButtonDelegate {
    func loginButton(_ button: LoginButton, didSucceedLogin loginResult: LoginResult) {
        hideIndicator()
        print("Login Succeeded.")
    }
    
    func loginButton(_ button: LoginButton, didFailLogin error: LineSDKError) {
        hideIndicator()
        print("Error: \(error)")
    }
    
    func loginButtonDidStartLogin(_ button: LoginButton) {
        showIndicator()
        print("Login Started.")
    }
}
```

https://developers.line.biz/en/reference/ios-sdk-swift/Protocols/LoginButtonDelegate.html

ここまでで動かしてみると、見た目はこんな感じ

<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/cc83fcbd-228c-7eb5-f7cf-a3521f8c4f74.png">
<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/2befaa56-1d57-e469-01cc-d95672bcbe37.png">
<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/e1cc5f1b-f4d3-9070-329d-210eee230bb0.png">
<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/08aaccd2-3eca-3214-5987-57fa08c0991a.png">
<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/bf86bbf3-7b0d-9fdc-2819-c6cbd2931cd6.png">

### ユーザープロフィールの取得

ログイン時にプロフィールの取得してみる

```swift
extension LoginViewController: LoginButtonDelegate {
    func loginButton(_ button: LoginButton, didSucceedLogin loginResult: LoginResult) {
        if let profile = loginResult.userProfile {
            print("User ID: \(profile.userID)")
            print("User Display Name: \(profile.displayName)")
            print("User Icon: \(String(describing: profile.pictureURL))")
        }
    }
}
```

`LoginButtonDelegate`を使わずに実装する場合

```swift

LoginManager.shared.login(permissions: [.profile], in: self) {
    result in
    switch result {
    case .success(let loginResult):
        if let profile = loginResult.userProfile {
            print("User ID: \(profile.userID)")
            print("User Display Name: \(profile.displayName)")
            print("User Icon: \(String(describing: profile.pictureURL))")
        }
    case .failure(let error):
        print(error)
    }
}
```

ちなみに、ログイン状態へのアクセスは`LoginManager.shared.isAuthorized`が使える

https://developers.line.biz/en/reference/ios-sdk-swift/Classes/LoginManager.html


### エラーを制御

一般的なエラー制御を参考に

https://developers.line.biz/ja/docs/ios-sdk/swift/error-handling/

```swift
extension LoginViewController: LoginButtonDelegate {
    func loginButton(_ button: LoginButton, didFailLogin error: LineSDKError) {
        if error.isUserCancelled {
            // User cancelled the login process himself/herself.
            
        } else if error.isPermissionError {
            // Equivalent to checking .responseFailed.invalidHTTPStatusAPIError
            // with code 403. Should login again.
            
        } else if error.isURLSessionTimeOut {
            // Underlying request timeout in URL session. Should try again later.
            
        } else if error.isRefreshTokenError {
            // User is accessing a public API with expired token, LINE SDK tried to
            // refresh the access token automatically, but failed (due to refresh token)
            // also expired. Should login again.
            
        } else {
            // Any other errors.
            print("\(error)")
        }
        print("Error: \(error)")
    }
}
```

https://developers.line.biz/en/reference/ios-sdk-swift/Enums/LineSDKError.html

### ユーザー情報をバックエンドサーバーで利用

> バックエンドサーバーでは、UserProfileオブジェクトから取得できるユーザーIDなどの情報は、利用しないでください。悪意のあるクライアントは、任意のユーザーになりすますために、任意のユーザーIDや不正な情報をバックエンドサーバーに送信できます。
ユーザーIDなどの情報を送信する代わりにアクセストークンを送信し、バックエンドサーバーではアクセストークンからユーザーIDなどの情報を取得します。

https://developers.line.biz/ja/reference/line-login/#verify-access-token

👇のドキュメントで安全なユーザー登録およびログインプロセスについて丁寧に説明されています

https://developers.line.biz/ja/docs/line-login/secure-login-process/

# おわりに
LINEログインの動きを確認するだけであれば、とても簡単でした。しかし実際の運用ではバックエンドへユーザー情報を送る必要があり、その場合はアクセストークンのやり取りが必須なため、簡単にはいかないのかと。
今回LINEログインを試したことで、[`アプリとサーバーの間で安全なログインプロセスを構築する`](https://developers.line.biz/ja/docs/line-login/secure-login-process/)について理解できたので大変収穫でした。

# 参考

https://developers.line.biz/ja/services/line-login/