[iOS版]QiitaのOAuth認証のやり方

# 概要
https://qiita.com/api/v2/docs

公開されているQiita APIを使用したQiitaのiOSアプリを作ろうとした時に，認証周りで少し苦戦したのでSwiftを使用したやり方をまとめてみます。Qiita APIを使用したiOSアプリを作ろうとしている人の参考になれば幸いです。

# 環境
- XCode14.2
- iOS15以上

# やり方
Qiita APIを使用した認証手順は以下のようになります。

1. Qiitaの認証ページを表示する
1. Qiitaの認証ページからアプリにリダイレクトされる
1. 2で渡されるコード，事前に発行済みのClientID・ClientSecretを使用してアクセストークンを発行する

上記の手順のやり方を説明していきます。

## Qiitaでアプリケーションの登録を行う

https://qiita.com/settings/applications

にアクセスします。

![スクリーンショット 2023-01-15 13.51.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/628850/d5bc54ff-d1e2-91d4-32d8-62c04562b2d7.png)

- アプリケーションの名前
    - 自分の作成するアプリケーションの名前
- アプリケーションの説明
    - 任意
- WebサイトのURL
    - 任意
- リダイレクト先のURL
    - (アプリケーションの名前)://(任意)

を設定して，アプリケーションの登録を行います。

その後，リダイレクト先のURLで指定した（アプリケーションの名前）をCustom URL Schemeで設定します。


https://dev.classmethod.jp/articles/ios-custom-url-scheme/

を参考に設定しました。


## Qiitaの認証ページを表示する

先ほど発行したClientID，ClientSecretを使用してQiitaの認証ページを表示します。
認証ページを表示するには，[ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession)を使用します。

https://developer.apple.com/documentation/authenticationservices/authenticating_a_user_through_a_web_service/

https://www.andyibanez.com/posts/using-aswebauthenticationaession-swiftui/

などが参考になりました。

実際のコードは以下のようになります。

```swift
import Foundation
import AuthenticationServices
import Combine

final class QiitaSignInViewModel: NSObject, ObservableObject {
    private var cancellables = Set<AnyCancellable>()

    private static var authURL: URL {
        var components = URLComponents()
        components.scheme = "https"
        components.host = "qiita.com"
        components.path = "/api/v2/oauth/authorize"
        components.queryItems = [
            "client_id": ClientID,
            "client_secret": ClientSecret,
            "scope": "read_qiita write_qiita"
        ].map { URLQueryItem(name: $0, value: $1) }
        return components.url!
    }
        
    func signIn() {
        let signInPromise = Future<URL, Error> { completion in            
            let authSession = ASWebAuthenticationSession(
                url: QiitaSignInViewModel.authUrl,
                callbackURLScheme: "qiinnreader"
            ) { (url, error) in
                if let error = error {
                    completion(.failure(error))
                } else if let url = url {
                    completion(.success(url))
                }
            }
            
            authSession.presentationContextProvider = self
            authSession.prefersEphemeralWebBrowserSession = true
            authSession.start()
        }
         signInPromise.sink(receiveCompletion: { completion in
            switch completion {
            case .failure(let error):
                print("error: \(error)")
            default:
                break
            }
        }, receiveValue: { _ in
            // アクセストークンを発行する処理
        })
         .store(in: &cancellables)
    }
}

extension QiitaSignInViewModel: ASWebAuthenticationPresentationContextProviding {
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        UIWindow()
    }
}
```

それぞれ説明していきます。

```swift
    private static var authURL: URL {
        var components = URLComponents()
        components.scheme = "https"
        components.host = "qiita.com"
        components.path = "/api/v2/oauth/authorize"
        components.queryItems = [
            "client_id": ArkanaKeys.Global().clientID,
            "client_secret": ArkanaKeys.Global().clientSecret,
            "scope": "read_qiita write_qiita"
        ].map { URLQueryItem(name: $0, value: $1) }
        return components.url!
    }
```

Qiitaの認証ページを表示するURLを返します。発行したClientID，ClientSecretをそれぞれ渡し，`scope`にスペース区切りで文字列で設定することに注意が必要です。　
　　

```swift
        let signInPromise = Future<URL, Error> { completion in
            let authSession = ASWebAuthenticationSession(
                url: QiitaSignInViewModel.authURL,
                callbackURLScheme: "qiinnreader"
            ) { (url, error) in
                if let error = error {
                    completion(.failure(error))
                } else if let url = url {
                    completion(.success(url))
                }
            }
            
            authSession.presentationContextProvider = self
            authSession.prefersEphemeralWebBrowserSession = true
            authSession.start()
        }
```

`presentationContextProvider`では，認証ページを表示する処理に関して設定します。ここでは，`self`を指定しているので，extensionで追加してあげます。

```swift
extension QiitaSignInViewModel: ASWebAuthenticationPresentationContextProviding {
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        UIWindow()
    }
}
```

https://developer.apple.com/documentation/authenticationservices/aspresentationanchor

より，iOS版を想定しているので，`UIWindow()`を返します。

`prefersEphemeralWebBrowserSession`では，Safariで保存されているデータを共有するかどうかを設定します。デフォルトのfalseのままだとSafariで保存されているcokkieを使用することができるようになってしまう（Safari上でQiitaのログインを行なっていた場合，ログイン処理を行うことなく認証が通るようになる）ので，ここではtrueを設定しておきます。  

## アクセストークンを発行する

Qiitaの認証ページで認証を行った後はアプリにリダイレクトされ，そのURLにはアクセストークンを発行するのに必要なcodeが含まれています。そのcodeを使用して，アクセストークンの発行を行います。


```swift
         signInPromise.sink(receiveCompletion: { completion in
            switch completion {
            case .failure(let error):
                print("error: \(error)")
            default:
                break
            }
        }, receiveValue: { _ in
            // アクセストークンを発行する処理
        })
         .store(in: &cancellables)
```

において，`アクセストークンを発行する処理`について説明します。

コードは以下のようになります。

```swift
    // 略
         signInPromise.sink(receiveCompletion: { completion in
            switch completion {
            case .failure(let error):
                print("error: \(error)")
            default:
                break
            }
        }, receiveValue: { [self] url in
            guard let code = retrieveCode(url) else {
                return
            }
            createAuthToken(code: code)
        })
         .store(in: &cancellables)
    }

    private func retrieveCode(_ url: URL) -> String? {
        let components: NSURLComponents? = getURLComonents(url)
        for item in components?.queryItems ?? [] {
            if item.name == "code" {
                return item.value?.removingPercentEncoding
            }
        }
        return nil
    }
    
    private func getURLComonents(_ url: URL) -> NSURLComponents? {
        var components: NSURLComponents? = nil
        components = NSURLComponents(url: url, resolvingAgainstBaseURL: true)
        return components
    }
```

リダイレクト後の`url`にはアクセストークンを発行するのに必要な`code`が含まれているので，`code`の取得を以下で行います。

```swift
    private func retrieveCode(_ url: URL) -> String? {
        let components: NSURLComponents? = getURLComonents(url)
        for item in components?.queryItems ?? [] {
            if item.name == "code" {
                return item.value?.removingPercentEncoding
            }
        }
        return nil
    }
    
    private func getURLComonents(_ url: URL) -> NSURLComponents? {
        var components: NSURLComponents? = nil
        components = NSURLComponents(url: url, resolvingAgainstBaseURL: true)
        return components
    }
```

https://bhoopendraumrao.medium.com/retrieving-query-parameters-from-deep-link-url-in-swift-98f69ef17f29

を参考にしました。

`code`を取得した後は，

https://qiita.com/api/v2/docs#post-apiv2access_tokens

に従って，`client_id`，`client_secret`,`code`を渡してアクセストークンを発行するだけです。


# 最後に

以上でSwiftを使用して，Qiita APIのOAuthを行う説明を終了します。`code`を取得するまでの方法が個人的には苦戦したので，その取得方法を主にまとめてみました。