【Swift】Vapor で Basic 認証を実装する

こんにちは。[kamimi](https://twitter.com/kamimi_01) です。🌞
[前回](https://qiita.com/kamimi01/items/0676f028670b8ff73c1e)に引き続き、Vapor を使った実装にハマっています。（二重の意味で）

今回は Vapor を使った API の Basic 認証の実装について書きたいと思います。

# 実装

こちらの公式ドキュメントに沿って、実装をしていきます。

https://docs.vapor.codes/security/authentication/#basic

## ユーザータイプ

まず、`Authenticatable`に準拠したユーザータイプを実装します。

```swift:User.swift
import Vapor

struct User: Authenticatable {
    let name: String
}
```

## オーセンティケーター

次に要となる認証の実装です。

公式ドキュメントでは、認証のミドルウェアを作成するためのヘルパーのプロトコルが２つ紹介されていました。**結論、今回は２つ目を使います。**

- `BasicAuthenticator`
- `AsyncBasicAuthenticator`

１つ目は、async/await 登場前に実装されたプロトコルです。
[SwiftNIO](https://github.com/apple/swift-nio) という Apple の OSS ライブラリを使って実装されています。ただ、以下のドキュメントにも詳しく書いてあるとおり、「大抵のコードが async/await を使用して書くことができるようになったため、基本的には asycn/await を使ってください」とあります。
`AsyncBasicAuthenticator`のコードコメントにも、`This is an async version of BasicAuthenticator`（これは BasicAuthenticator の async バージョンです）と記載がありました。

https://docs.vapor.codes/basics/async/

ということで今回は `AsyncBasicAuthenticator`のプロトコルを使用して実装します。


```swift:UserAuthenticator.swift
import Vapor

class UserAuthenticator: AsyncBasicAuthenticator {
    func authenticate(basic: Vapor.BasicAuthorization, for request: Vapor.Request) async throws {
        // username と password をハードコードしているが、実際はこれやってはだめ
        if basic.username == "test" && basic.password == "secret" {
            request.auth.login(User(name: "Vapor"))
        }
    }
}
```

## 呼び出し方

ミドルウェアとして、各ルートにいく前に認証を挟むように実装します。
`grouped(_ middleware: Middleware...)`は、ミドルウェアをラップしてルーターを返すメソッドです。以下のように可変長の引数を渡すことができます。

```swift
let group = router.grouped(FooMiddleware(), BarMiddleware())
```

今回は認証のミドルウェアとして、先ほど実装した`UserAuthenticator`のみを渡します。

```swift:router.swift
import Vapor

func routes(_ app: Application) throws {
    let protected = app.grouped(UserAuthenticator())

    protected.post("authTest") { req in
        try req.auth.require(User.self).name
    }
}
```

# 結果

実装できたので、ちゃんと認証が実装されているか確認します。
リクエストしている API は私が最近実装中の API です。

## 認証失敗

以下のようにリクエストしてみました。Authorization ヘッダーなしでのリクエストです。

```shell:cURL でのリクエスト結果
$ curl -v --location --request POST 'http://127.0.0.1:8080/postAppStoreState' \
--header 'Content-Type: application/json' \
--data-raw '{
    "channelID": "C01JJKQPKCK",
    "appIDs": [
        "1673161138", "1668609447", "1668244395", "1667179734"
    ]
}'
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /postAppStoreState HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.87.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 120
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< content-length: 38
< content-type: application/json; charset=utf-8
< connection: keep-alive
< date: Fri, 28 Apr 2023 22:52:28 GMT
<
* Connection #0 to host 127.0.0.1 left intact
{"error":true,"reason":"Unauthorized"}
```

ステータスコードは`401 Unauthorized`が返ってきています。
ボディは

```json
{"error":true,"reason":"Unauthorized"}
```
でレスポンスされていることがわかります。


## 認証成功

確認するまでもないかと思いますが、一応こちらの結果も載せておきます。ちゃんと`Authorization`ヘッダーをつけます。

```shell:cURL でのリクエスト結果
curl -v --location --request POST 'http://127.0.0.1:8080/postAppStoreState' \
--header 'Authorization: Basic dGVzdDpzZWNyZXQ=' \
--header 'Content-Type: application/json' \
--data-raw '{
    "channelID": "C01JJKQPKCK",
    "appIDs": [
        "1673161138"
    ]
}'
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /postAppStoreState HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.87.0
> Accept: */*
> Authorization: Basic dGVzdDpzZWNyZXQ=
> Content-Type: application/json
> Content-Length: 78
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: application/json; charset=utf-8
< content-length: 290
< connection: keep-alive
< date: Fri, 28 Apr 2023 22:56:45 GMT
<
* Connection #0 to host 127.0.0.1 left intact
{"channelID":"C01JJKQPKCK","appIDs":["1673161138"],"postMessage":"iOS アプリのステータスをお知らせします :apple:\n【TopicGen】\nバージョン：1.1\nステータス：販売準備完了（READY_FOR_SALE） :ok_hand::moneybag:\n作成日時：2023\/02\/21 20:51:45\n\n"}
```

ステータスコードは`200 OK`、
ボディは

```json
{
    "channelID": "C01JJKQPKCK",
    "appIDs": [
        "1673161138"
    ],
    "postMessage": "iOS アプリのステータスをお知らせします :apple:\n【TopicGen】\nバージョン：1.1\nステータス：販売準備完了（READY_FOR_SALE） :ok_hand::moneybag:\n作成日時：2023/02/21 20:51:45\n\n"
}
```
ということで、想定したレスポンスが返ってきています。

# おわりに

この認証の実装だけではないのですが、Vapor のドキュメントはとても充実していてわかりやすいです。日本語はないのでそこは頑張って読む必要があります。

どのサーバーサイドフレームワークも同様なのかもしれないですが、各ルートにいく前にミドルウェアを挟めたりする点は、express とかと一緒なんだなあと、初心者的感想を持ちました。（express のミドルウェア実装は[こちら](https://expressjs.com/ja/guide/using-middleware.html)）

# 参考

https://docs.vapor.codes/security/authentication/#basic