Help us understand the problem. What is going on with this article?

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

経緯

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

今回の内容

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

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

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

参照

 LINE WORKS APIについては「LINE WORKS Developers」にいろいろ書かれています。

実装

準備

 先に「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」に以下の内容を追加します。

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のパラメータクラスを以下のように追加

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行を追加

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

認証エラーのための例外作成

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

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;
        }
    }
}

処理するクラスのサービスインターフェース

 サービスにするためにインターフェースを作成します。

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

接続用のクラス

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

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トークンとして設定する必要があります。

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();
        }
    }
nosa67
老害の高齢SEでーす。 おもにC#で業務アプリ作ってます。 今いる派遣先は開発をやめるとのことですので、だれか拾ってください。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away