個人情報を含むデータをAPIでやり取りする際に、
アクセストークンを発行し、認証する段階を踏むことでセキュリティ強度を確保したい。
※JWTを扱うために、System.IdentityModel.Tokens.Jwt というNuGetパッケージをインストールする
取得データのやり取りの前にアクセストークンを発行し、以降の通信時にはこのアクセストークンをBearer認証にしようする。
アクセストークン発行
リクエスト側
・メソッド:POST
・ボディ:JSON
{
"PersonalID": "xxxxxxxxxxxxxxxxxxx"
}
レスポンス側
1.JWTのシークレットキーと署名クレデンシャルを定義する
2.クレーム(JWTのペイロードに含めるデータ)を作成する
3.トークンを発行する
4.生成したトークンをレスポンスする
※レイヤードアーキテクチャ(Controller,DataSet,Param,Resに分かれている)で実装
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
CreateTokenParam.vb
Imports System.Runtime.Serialization
<DataContract([Namespace]:="")>
Public Class CreateTokenParam
<DataMember()>
Public Property PersonalID As String
End Class
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 アクセストークンの値」をセットする
レスポンス側
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
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
{
"Result": "True",
"ErrorCd": "",
"ErrorMessage": "",
"PersonalInfoList": [
{
"PersonalID": 1,
"Name": "桃太郎",
"Age": 1
}
]
}