ASP.NET Core3.1でLINE WORKSのサーバーAPIの認証をしてみた(「RSA SHA-256」で暗号化したJWTで苦労した)

#経緯
　ちょっとLINE WORKSと連携して作ろうかと思って、無料で使える範囲で接続して何ができるのか調べてました。結果、私のしたいことができそうにないのであきらめたのですが、認証を通すために苦労したので、いつか使うかもしれない時のメモ書きとして残しときます。
　ほかでも「RSA SHA-256」で暗号化したJWTを送信するときに参考になるかと思います。

#今回の内容
　まず、このページを見るということは、LINE WORKSは何かわかっていると思いますが、どうやらＡＰＩは大きく以下の2つのパートに分かれているようです。

- サーバーAPI：個々のメンバーのデータを処理しない。
- サービスAPI：個々のメンバーのログインを必要とする。

　このうち、無料で使えるのはサーバーAPIの中のトークBot関連の処理のみです。この処理は、最初にサーバーに認証を得てトークンを取得する必要があるのですが、この時の処理の実装にちょっと苦労したので、それを記載しています。実際のBotは作ってませんし、作りませんのであしからず。

#参照
　LINE WORKS APIについては[「LINE WORKS Developers」](https://developers.worksmobile.com/jp/?lang=ja)にいろいろ書かれています。

#実装

## 準備
　先に「LINE WORKS」の「Developer Console」で「API ID」と「Server List(ID登録タイプ)」を発行してください。「Server List(固定IPタイプ)」の場合はおそらく認証ももう少し楽なのですが、開発時の事などを考えると固定IPは難しいので、たいていは「Server List(ID登録タイプ)」を使うかと思います。

　※　認証までなら「Server API Consumer Key」は不要です。以降の処理をするには必要です。

##NuGetで追加するパッケージ
「System.IdentityModel.Tokens.Jwt」を追加してください。

##パラメータの設定
　「appsettings.json」に以下の内容を追加します。

```json:appsettings.json
"LINE": {
    "ApiId": "「Developer Console」の「API ID」",
    "ServerID": "「Developer Console」の「Server List(固定IPタイプ)」の「ID」",
    "PrivateKey": "「Developer Console」の「Server List(固定IPタイプ)」の認証キーでヘッダ「-----BEGIN RSA PUBLIC KEY-----」とフッタ「-----END RSA PUBLIC KEY-----」を除いて改行を除いて1行にしたもの"
  }
```

## パラメータクラス
　LINEのパラメータクラスを以下のように追加

```C#:LINEParams.cs
namespace CareScheduler.Areas.Identity.LINEWORKS
{
    public class LINEParams
    {
        public string ApiId { get; set; }
        public string ServerID { get; set; }
        public string PrivateKey { get; set; }
    }
}
```

　「Startup.cs]の「ConfigureServices()」に以下の1行を追加

```C#:Startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            ...
            // LINE パラメータのDI設定
            services.Configure<LINEParams>Configuration.GetSection("LINE"));
            ...
        }
```

##認証エラーのための例外作成
　認証時のエラーの為に以下の例外クラスを作っておきました。このあたりはお好きなように。

```C#:LineWorksAuthenticationException.cs
namespace CareScheduler.Areas.Identity.LINEWORKS
{
    public class LineWorksAuthenticationException : Exception
    { 
        public string ErrorCode { get; set; }

        public string Detail { get; set; }

        public LineWorksAuthenticationException(LineWorksAuthenticationError jsonError):base(jsonError.Message)
        {
            ErrorCode = jsonError.Code;
            Detail = jsonError.Detail;
        }
    }
}
```

##処理するクラスのサービスインターフェース
　サービスにするためにインターフェースを作成します。

```C#:ILINEWORKSServer.cs
namespace CareScheduler.Areas.Identity.LINEWORKS
{
    /// <summary>
    /// LINE WORKS サーバーAPIを利用するクラスのインターフェース（DI用）
    /// </summary>
    public interface ILINEWORKSServer
    {
        string GetAccessToken();
    }
}
```

##接続用のクラス
　実行クラスは以下の通りです。
　このクラスを実施するところでＤＩで取り込んで、GetAccessToken()を実行すると、アクセストークンが取得できます。
　ネットでいろいろ探して、ようやく「System.Security.Cryptography.RSA」を利用してシンプルに送信用トークンの署名に利用する「signingCredentials」が作れました。認証キーを取り込む際に、認証キーのヘッダとフッタを外すことと、取込みメソッドが「ImportPkcs8PrivateKey()」であるのにくづくまで少しかかりました。
　これ以外でも外部でRSAでトークン作成のための認証キーを利用することがあるかなーと思います。

```C#:ILINEWORKSServer.cs
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using System;
using System.Collections.Specialized;
using System.Net;
using System.Security.Cryptography;
using System.Text;

namespace CareScheduler.Areas.Identity.LINEWORKS
{
    /// <summary>
    /// LINE WORKS サーバーAPIを利用するクラスのインターフェース（DI用）
    /// </summary>
    public interface ILINEWORKSServer
    {
        string GetAccessToken();
    }

    /// <summary>
    /// LINE WORKS サーバーAPIを利用するクラス
    /// </summary>
    public class LINEWORKSServer : ILINEWORKSServer
    {
        // 認証処理のグラント
        const string GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";

        // 認証処理APIのURL
        string _authenticationUrl = "https://auth.worksmobile.com/b/{API ID}/server/token";

        /// <summary>
        /// LINE WORKS サーバーAPI用のパラメータ
        /// </summary>
        LINEParams _lineParams;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="lineParams"></param>
        public LINEWORKSServer(IOptions<LINEParams> lineParams)
        {
            _lineParams = lineParams.Value;
            _authenticationUrl = _authenticationUrl.Replace("{API ID}", _lineParams.ApiId);
        }

        /// <summary>
        /// LINE WORKS サーバーAPのアクセストークンを取得する
        /// </summary>
        /// <returns></returns>
        public string GetAccessToken()
        {
            // RSA クラスを利用して認証キーから署名情報を作成する
            using RSA rsa = RSA.Create();
            rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(_lineParams.PrivateKey), out _);
            var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);

            // トークン文字列の作成の生成
            var descriptor = new SecurityTokenDescriptor
            {
                Issuer = _lineParams.ServerID,
                SigningCredentials = signingCredentials,
                IssuedAt = DateTime.UtcNow,
                Expires = (DateTime.UtcNow).AddMinutes(10),
            };
            var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
            var token = handler.CreateJwtSecurityToken(descriptor);
            var tokenString = handler.WriteToken(token);

            // LINE WORKSのサーバーAPIの認証APIにPOST
            var client = new WebClient() { Encoding = Encoding.UTF8 };
            var content = new NameValueCollection();
            content["assertion"] = tokenString;
            content["grant_type"] = GRANT_TYPE;
            string response = Encoding.UTF8.GetString(client.UploadValues(_authenticationUrl, "POST", content));

            // 認証結果を取得
            var result = JsonConvert.DeserializeObject<LineWorksAuthenticationResultToken>(response);
            if (string.IsNullOrEmpty(result.AccessToken))
            {
                throw new LineWorksAuthenticationException(JsonConvert.DeserializeObject<LineWorksAuthenticationError>(response));
            }
            return result.AccessToken;
        }

        /// <summary>
        /// LINE WORKS サーバーAPIのアクセストークン取得結果のJSONデータ取得用クラス
        /// </summary>
        class LineWorksAuthenticationResultToken
        {
            [JsonProperty("access_token")]
            public string AccessToken { get; set; }

            [JsonProperty("token_type")]
            public string TokenType { get; set; }

            [JsonProperty("expires_in")]
            public int ExpiresIn { get; set; }
        }

    }

    public class LineWorksAuthenticationError
    {
        [JsonProperty("code")]
        public string Code { get; set; }

        [JsonProperty("message")]
        public string Message { get; set; }

        [JsonProperty("detail")]
        public string Detail { get; set; }
    }
}
```

##利用方法
　使うクラスのコンストラクタ引数に「ILINEWORKSServer lineWork」を追加して「LINEWORKSServer」の「GetAccessToken()」を実行します。
　以下は「Index.cshtml」のRazorPageのモデルのプロパティ「APIToken」に取り込んでみるサンプルです。botを操作するには、ここで得たトークンをAPI呼び出しでHTMLヘッダにBearerトークンとして設定する必要があります。

```C#:Index.cshtml.cs
public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public string APIToken { get; set; }
        ILINEWORKSServer _lineWorks;

        public IndexModel(ILogger<IndexModel> logger, ILINEWORKSServer lineWorks)
        {
            _logger = logger;
            _lineWorks = lineWorks;
        }

        public void OnGet()
        {
            APIToken = _lineWorks.GetAccessToken();
        }
    }
```