JWTを用いてAPI使用時にアクセストークンを発行し、セキュリティ強度を確保する方法

個人情報を含むデータをAPIでやり取りする際に、
アクセストークンを発行し、認証する段階を踏むことでセキュリティ強度を確保したい。

※JWTを扱うために、System.IdentityModel.Tokens.Jwt というNuGetパッケージをインストールする

取得データのやり取りの前にアクセストークンを発行し、以降の通信時にはこのアクセストークンをBearer認証にしようする。

# アクセストークン発行
**リクエスト側**
・メソッド：POST
・ボディ：JSON
```JSON
{
    "PersonalID": "xxxxxxxxxxxxxxxxxxx"
}
```

**レスポンス側**
1.JWTのシークレットキーと署名クレデンシャルを定義する
2.クレーム（JWTのペイロードに含めるデータ）を作成する
3.トークンを発行する
4.生成したトークンをレスポンスする


※レイヤードアーキテクチャ（Controller,DataSet,Param,Resに分かれている）で実装
```VB:CreateTokenController.vb
Imports System.Web.Http
Imports System.Text.Json
Imports System.IdentityModel.Tokens.Jwt
Imports Microsoft.IdentityModel.Tokens
Imports System.Security.Claims
Imports System.Security.Cryptography
Imports System.Text

''' <summary>
''' アクセストークン取得API
''' </summary>
Public Class CreateTokenController
    Public Function PostValue(<FromBody()> param As CreateTokenControllerParam) As CreateTokenControllerRes

        Dim res As New CreateTokenControllerRes

        Try

            'パラメータチェック
            If CheckParam(param) = False Then
                res.Result = False
                res.ErrorCd = "E01"
                res.ErrorMessage = "パラメータが不正です。"
                Return res
            End If

            'トークンを生成
            Dim token As String = GenerateJwtToken(PersonalID)

            res.AccessToken = token
            res.Expiration = 3600
            res.TokenType = "Bearer"
            res.Result = True

        Catch ex As Exception
            res.Result = False
            res.ErrorCd = "E99"
            res.ErrorMessage = "予期せぬエラーが発生（" & ex.Message & "）"
        End Try
        Return res
    End Function


    ''' <summary>
    ''' パラメータチェック
    ''' </summary>
    ''' <param name="param"></param>
    ''' <returns></returns>
    Private Function CheckParam(ByVal param As SE02_04Param) As Boolean
        If param Is Nothing Then
            Return False
        End If

        If String.IsNullOrEmpty(param.PersonalID) Then
            Return False
        End If

        Return True
    End Function


    ''' <summary>
    ''' アクセストークン生成
    ''' </summary>
    ''' <param name="contClientCd"></param>
    ''' <param name="pbxCd"></param>
    ''' <returns></returns>
    Private Function GenerateJwtToken(PersonalID As String) As String
        ' クレームの作成
        Dim claims As New List(Of Claim) From {
                New Claim("PersonalID", personalID)
            }

        ' シークレットキー
        Dim key As New SymmetricSecurityKey(Encoding.UTF8.GetBytes(CommonConst.JWT_KEY))

        ' トークン署名用の資格情報
        Dim credentials As New SigningCredentials(key, SecurityAlgorithms.HmacSha256)

        ' トークンの生成
        Dim token As New JwtSecurityToken(
                issuer:="MyApp",                ' 発行者
                audience:="YourApp",              ' 受信者
                claims:=claims,                   ' クレームのリスト
                expires:=DateTime.Now.AddSeconds(3600), ' 有効期限（3600s）
                signingCredentials:=credentials   ' 署名情報
            )

        ' トークンを文字列として返す
        Return New JwtSecurityTokenHandler().WriteToken(token)
    End Function
End Class

```
```VB:CreateTokenParam.vb
Imports System.Runtime.Serialization
<DataContract([Namespace]:="")>
Public Class CreateTokenParam
    <DataMember()>
    Public Property PersonalID As String
End Class
```
```VB:CreateTokenRes.vb
Imports System.Runtime.Serialization

<DataContract([Namespace]:="")>
Public Class CreateTokenRes
    <DataMember()>
    Public Property Result As Boolean = True

    <DataMember()>
    Public Property ErrorCd As String

    <DataMember()>
    Public Property ErrorMessage As String

    <DataMember()>
    Public Property AccessToken As String

    <DataMember()>
    Public Property Expiration As Integer

    <DataMember()>
    Public Property TokenType As String


    Public Sub New()
        Result = False
        ErrorCd = String.Empty
        ErrorMessage = String.Empty
        AccessToken = String.Empty
        Expiration = 0
        TokenType = String.Empty
    End Sub
End Class
```
# APIを使用してデータを取得
**リクエスト側**
・Bearer認証を用いる
・メソッド：GET or POST（例ではGETを使用）
・Authorizationヘッダーかカスタムヘッダーに「Beare アクセストークンの値」をセットする

**レスポンス側**
```VB:PersonalInfoAPIController.vb
Imports System.Text
Imports System.IdentityModel.Tokens.Jwt
Imports Microsoft.IdentityModel.Tokens
Imports System.Security.Claims
Imports System.Net.Http
Imports System.Web.Http

Public Class PersonalInfoAPIController

    Public Function GetValue() As PersonalInfoAPIRes

        Dim res As New PersonalInfoRes

        Try
            ' カスタムヘッダー名 My-Authorization からトークンを取得する
            Dim authHeader As IEnumerable(Of String)

            If Not Request.Headers.TryGetValues("My-Authorization", authHeader) Then
                res.Result = False
                res.ErrorCd = "E99"
                res.ErrorMessage = "ヘッダーを取得できませんでした。"
                Return res
            End If

            ' 最初の値を取得
            Dim token As String = authHeader.FirstOrDefault()

            ' Authorizationヘッダーが存在しない、または形式が正しくない場合は認証失敗
            If String.IsNullOrEmpty(token) OrElse Not token.StartsWith("Bearer ") Then
                res.Result = False
                res.ErrorCd = "E99"
                res.ErrorMessage = "Authorizationヘッダーが存在しない、または形式が正しくありません。"
                Return res
            End If

            ' Bearer トークン部分のみを抽出
            token = token.Substring("Bearer ".Length).Trim()

            ' トークンを検証する
            Dim validatedToken As SecurityToken
            Dim claimsPrincipal As ClaimsPrincipal = JwtTokenAuthUtil.ValidateJwtToken(token, validatedToken)

            ' トークンが無効であれば認証失敗
            If claimsPrincipal Is Nothing Then
                res.Result = False
                res.ErrorCd = "E03"
                res.ErrorMessage = "トークンの認証に失敗しました。"
                Return res
            End If

            ' クレームからPersonalIDを取得
            Dim claim As Claim = claimsPrincipal.FindFirst("PersonalID")
            Dim personalID As String = String.Empty
            If claim IsNot Nothing Then
                personalID = claim.Value
            End If

            '内線グループ取得
            Dim personalInfoList As List(Of PersonalInfoList) = GetPersonalInfo(dbCnStr, personalID)

            res.Result = True
            res.PersonalInfoList = personalInfoList

        Catch ex As Exception
            res.Result = False
            res.ErrorCd = "E99"
            res.ErrorMessage = "予期せぬエラーが発生しました。"
        End Try

        Return res

    End Function


    Private Function GetPersonalInfo(dbCnStr As String, personalID As String) As List(Of PersonalInfoList)
        '初期化
        Dim list As New List(Of PersonalInfoList)

        Using ta As New dsPersonalInfoTableAdapters.T_PersonalInfoTableAdapter(dbCnStr)
            Dim dt As New dsPersonalInfo.T_PersonalInfoDataTable
            ta.Fill(dt, personalID)
            For Each dr As dsPersonalInfo.T_PersonalInfoRow In dt

                Dim PersonalID = dr.PersonalID
                Dim Name = dr.Name
                Dim Age = dr.Age

                list.Add(New PersonalInfoList(PersonalID, Name, Age))
            Next
        End Using

        Return list
    End Function

End Class
```
```VB:PersonalInfoAPIRes.vb
Imports System.Runtime.Serialization

<DataContract([Namespace]:="")>
Public Class PersonalInfoAPIRes
	<DataMember()>
	Public Property Result As Boolean = True

	<DataMember()>
	Public Property ErrorCd As String

	<DataMember()>
	Public Property ErrorMessage As String

	<DataMember()>
	Public Property PersonalInfoList As List(Of PersonalInfoList)

	Public Sub New()
		Result = False
		ErrorCd = String.Empty
		ErrorMessage = String.Empty
	End Sub

End Class

Public Class PersonalInfoList
	Public Sub New(personalID As String,
				   name As String,
				   age As Integer)

		Me.PersonalID = personalID
		Me.Name = name
		Me.Age = age
	End Sub

	Public Property PersonalID As String

	Public Property Name As String

	Public Property Age As Integer

End Class
```
```JSON
{
    "Result": "True",
    "ErrorCd": "",
    "ErrorMessage": "",
    "PersonalInfoList": [
        {
            "PersonalID": 1,
            "Name": "桃太郎",
            "Age": 1
        }
    ]
}
```