Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
0
Help us understand the problem. What are the problem?
@Rambosan

PowerApps/PowerAutomateのカスタムコネクタでOAuth1.0のAPIに接続する

概要

PowerApps/PowerAutomateで公式にコネクタが提供されていないサービス(API)を利用するには、カスタムコネクタを作成する必要があります。
カスタムコネクタはOAuth1.0認証には対応しておらず、どうしても接続したい場合はAzure Functionsなどの自前のAPIを介する必要がありました。
今回はカスタムコネクタだけでOAuth1.0のAPIに接続してみます。

image.png

実現方法

カスタムコネクタのアップデートにより、C#のカスタムコード が追加されましたので、この機能を利用します。
プレビュー機能につき継続的に動作する保証はありません。

C#のコードを使ってAuthorizationヘッダを生成し、リクエスト時に付加するだけです。
image.png
カスタムコネクタが呼び出されると、Context.RequestにHttpRequestMessageとしてアプリなどから受け取ったリクエスト情報が格納され、これをSendAsync(Context.Request)の形で明示的にAPI要求します。
このHttpRequestMessageに手を加えるといった仕組みです。

OAuth1.0に接続する場合、Authorizationヘッダは以下のようになります。
oauth_consumer_keyは管理ページから取得でき固定なのですが、oauth_nonceやoauth_signatureなどは接続の都度計算する必要があります。

Authorization : OAuth oauth_consumer_key="",oauth_nonce="",oauth_signature="",oauth_signature_method="HMAC-SHA1",oauth_timestamp="",oauth_token="",oauth_verifier="",oauth_version="1.0"

仕組み上、トークン情報を直接コード内に記述する必要があり誤って公開するリスクがありますので自己責任でお願いします。

準備

今時OAuth1.0しか対応していないAPIなんてそんなにないと思いますが、
今回は家計簿アプリのZaim APIを例題に実現してみます。

開発者登録

先人たちの記事を参考に開発者登録しておきます。
https://qiita.com/shutosg/items/6845057432bca551024b

以下が管理ページから取得できる情報です。
トークンの有効期限は永続的に設定します。
image.png

トークンパラメータの取得

カスタムコネクタ画面からユーザー認証画面は出せないので、事前に認証に必要なパラメータを取得しておく必要があります。
以下の5つです。

  • consumerKey(開発者ページから取得)
  • consumerSecret(開発者ページから取得)
  • token
  • tokenSecret
  • verifier

取得方法は以下のいずれかを使用します。

①POSTMANを使用する場合

②Pythonで作られている方のものを使用する場合

③雑に作ったC#コンソールアプリで取得する場合
    class Program {

        static RestClient _client;

        static string _requestTokenUrl = "https://api.zaim.net/v2/auth/request";
        static string _userAuthUrl = "https://auth.zaim.net/users/auth";
        static string _accessTokenUrl = "https://api.zaim.net/v2/auth/access";
        static string _callbackUrl = "http://localhost:4336/";
        static string _comsumerKey = "{Your Key}";
        static string _comsumerSecret = "{Your Key}";


        static string _accessToken;
        static string _tokenSecret;
        static string _verifier;

        async static Task Main(string[] args) {

            _client = new RestClient() {
                Authenticator = new OAuth1Authenticator()
            };

            // Authentication workflow

            //token , token secretの取得 
            await GetTokens();

            //ユーザー認証用URLにアクセス
            LaunchUserAuthorization();

            Console.WriteLine("リダイレクトされたURLを入力してください。 ex." + _callbackUrl + "?oauth_token=~");
            var callbackedUrl = Console.ReadLine();

            //verifierの取得
            GetVerifier(callbackedUrl);

            //取得したtokenの検証
            if (await ValidateAccessToken() != true) return;

            var oauthParams = $@"OAuth1.0のトークン情報です。
comsumerKey     :   {_comsumerKey}
comsumerSecret  :   {_comsumerSecret}
accessToken     :   {_accessToken}
tokenSecret     :   {_tokenSecret}
verifier        :   {_verifier}
";
            Console.Write(oauthParams);
            Console.ReadLine();
        }


        static async Task GetTokens() {

            _client.Authenticator = OAuth1Authenticator.ForRequestToken(_comsumerKey, _comsumerSecret, _callbackUrl);

            var request = new RestRequest(_requestTokenUrl, Method.GET);
            var reponse = await _client.ExecuteAsync(request);

            var parameters = HttpUtility.ParseQueryString(reponse.Content);

            if (parameters.TryGet("oauth_token", out _accessToken) == false) {
                throw new Exception(nameof(_accessToken) + "の取得に失敗しました");
            }

            if (parameters.TryGet("oauth_token_secret", out _tokenSecret) == false) {
                throw new Exception(nameof(_tokenSecret) + "の取得に失敗しました");
            }
        }

        static void LaunchUserAuthorization() {
            System.Diagnostics.Process.Start(_userAuthUrl + "?oauth_token=" + _accessToken);
        }
        static void GetVerifier(string url) {
            var uri = new Uri(url);
            NameValueCollection query = HttpUtility.ParseQueryString(uri.Query);

            if (query.TryGet("oauth_verifier", out _verifier) == false) {
                throw new Exception("tokenの取得に失敗しました");
            }
        }

        static async Task<bool> ValidateAccessToken(){


            _client.Authenticator = OAuth1Authenticator.ForAccessToken(_comsumerKey, _comsumerSecret, _accessToken, _tokenSecret, _verifier);

            var request = new RestRequest(_accessTokenUrl, Method.GET);
            var reponse = await _client.ExecuteAsync(request);

            if (reponse.StatusCode != HttpStatusCode.OK) {
                throw new Exception("Tokenの検証に失敗しました。" + reponse.Content );
            }

            return true;
        }

    }

    public static class Extentions {
        public static bool TryGet(this NameValueCollection nameValueCollection, string keyName, out string value) {

            var nameValue = nameValueCollection[keyName];

            if (nameValue == null) {
                value = null;
                return false;
            }
            value = nameValue;
            return true;
        }
    }


う~ん、複雑・・・

カスタムコネクタを使って接続

カスタムコネクタの作成

こちらの記事を参考にカスタムコネクタを作成します。
https://qiita.com/Rambosan/items/7cda28bac81da794b935

認証は無しで作成。
定義画面で各アクションのエンドポイントや要求スキーマを作成しておきます。
image.png

カスタムコードに貼り付けるコード

リクエスト時にAuthorizationヘッダを生成して付加するだけですので、アクション(エンドポイント)によって処理を分ける必要はありません。右から左に流すだけです。

カスタムコードはAzure Functionsのように外部ライブラリは利用できませんので、
RestSharpの実装を参考にカスタムコネクタで使えるように書きました。
業界未経験者の糞コードです。

oauth_signature_methodはHMAC-SHA1のみ対応です。

    public class Script:ScriptBase {

        public override async Task<HttpResponseMessage> ExecuteAsync() {

            // Define your oauth1.0 key parameters
            string _consumerKey = "{YourParam}";
            string _consumerSecret = "{YourParam}";
            string _token = "{YourParam}";
            string _tokenSecret = "{YourParam}";
            string _verifier = "{YourParam}";

            // Build OAuth1.0 Authorization.
            var auth = new OAuth1AuthorizationBuilder(this.Context.Request, _consumerKey, _consumerSecret, _token, _tokenSecret, _verifier);
            Context.Request.Headers.Authorization = new AuthenticationHeaderValue("OAuth", auth.ToString());

            // Send request to backend api.
            var result = await Context.SendAsync(this.Context.Request, CancellationToken);

            return result;
        }

        /// <summary>
        /// OAuth1.0のAuthorizationを生成します。HMAC-SHA1方式固定です。
        /// </summary>
        public class OAuth1AuthorizationBuilder {

            private HttpRequestMessage _requestMessage = new HttpRequestMessage();
            private List<WebPair> _authParameters;

            private string _tokenSecret = "";
            private string _consumerSecret = "";

            public OAuth1AuthorizationBuilder(HttpRequestMessage requestMessage,
                string consumerKey, string consumerSecret,
                string token, string tokenSecret,
                string verifier) {

                _requestMessage = requestMessage;

                //必要なパラメータをListに定義
                _authParameters = new List<WebPair>() {
                    new WebPair("oauth_consumer_key",consumerKey,true),
                    new WebPair("oauth_token",token,true),
                    new WebPair("oauth_verifier",verifier,true),
                    new WebPair("oauth_version","1.0"),
                    new WebPair("oauth_signature_method","HMAC-SHA1")
                };

                // Secret
                _tokenSecret = tokenSecret;
                _consumerSecret = consumerSecret;

                // timestamp
                var timeStamp = GetTimestamp();
                _authParameters.Add(new WebPair("oauth_timestamp", timeStamp));

                // nonce
                var nonce = GetNonce();
                _authParameters.Add(new WebPair("oauth_nonce", nonce));

                // Generate signature
                var signatureBase = ConcatenateRequestElements(_requestMessage.RequestUri, _requestMessage.Method.ToString(), _authParameters);
                var _signature = GetHmacSignature(new HMACSHA1(), _consumerSecret, _tokenSecret, signatureBase);
                _authParameters.Add(new WebPair("oauth_signature", Encode(_signature)));

            }

            public override string ToString() {
                var authHeaders = _authParameters.Select(x => $"{x.Name}=\"{x.Value}\"");

                return string.Join(",", authHeaders);
            }

            private string Encode(string value) {
                return Uri.EscapeDataString(value);
            }

            static readonly Random Random = new Random();
            static readonly object RandomLock = new object();

            private string GetNonce() {

                const string chars = "1234567890abcdefghijklmnopqrstuvwxyz";
                var nonce = new char[16];

                //英数字の中からランダムに16文字を生成する。
                lock (RandomLock) {
                    for (var i = 0; i < nonce.Length; i++) nonce[i] = chars[Random.Next(0, chars.Length)];
                }

                return new string(nonce);

            }

            private string GetTimestamp() {
                var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
                return timeSpan.TotalSeconds.ToString();
            }

            private string GetHmacSignature(KeyedHashAlgorithm crypto, string consumerSecret, string tokenSecret, string signatureBase) {
                //暗号化キーを設定
                var key = $"{consumerSecret}&{tokenSecret}";
                crypto.Key = Encoding.UTF8.GetBytes(key);

                //signatureBaseをバイト変換してハッシュを計算
                var hash = crypto.ComputeHash(Encoding.UTF8.GetBytes(signatureBase));

                return Convert.ToBase64String(hash);

            }

            private string ConcatenateRequestElements(Uri requestUri, string methodName, IEnumerable<WebPair> webPairs) {

                // method name as upper case
                var method = methodName.ToUpper();

                // encoded uri 
                var baseUri = Encode(requestUri.GetLeftPart(UriPartial.Path));

                //var signatureParam = webPairs.Where(x => x.IsAuthHeader == true).ToList();
                var signatureParam = webPairs.ToList();

                // add query name-value to signatureParam
                var queryNameValue = HttpUtility.ParseQueryString(requestUri.Query);
                signatureParam.AddRange(queryNameValue.AllKeys.Select(x => new WebPair(x, queryNameValue[x])));

                // sort and join signature params
                string parameters = Uri.EscapeDataString(string.Join("&", SortParameters(signatureParam)));

                return $"{method}&{baseUri}&{parameters}";
            }

            /// <summary>
            /// List<WebPair>を昇順でソートします。
            /// </summary>
            /// <param name="parameters"></param>
            /// <returns></returns>
            private IEnumerable<string> SortParameters(List<WebPair> parameters) {
                return parameters
                .Select(x => new WebPair(x.Name, x.Value, x.Encode))
                .OrderBy(x => x, WebPair.Comparer)
                .Select(x => $"{x.Name}={x.Value}");
            }

            class WebPair {
                public WebPair(string name, string value, bool encode = false) {
                    Name = name;
                    Value = value;
                    WebValue = encode ? Uri.EscapeDataString(value) : value;
                    Encode = encode;
                }

                public string Name { get; }
                public string Value { get; }
                public string WebValue { get; }
                public bool Encode { get; }

                internal static WebPairComparer Comparer { get; } = new WebPairComparer();

                internal class WebPairComparer : IComparer<WebPair> {
                    public int Compare(WebPair x, WebPair y) {
                        var compareName = string.CompareOrdinal(x.Name, y.Name);

                        return compareName != 0 ? compareName : string.CompareOrdinal(x.Value, y.Value);
                    }
                }
            }

        }

    }

テストと応答スキーマ

ここからは通常のカスタムコネクタと同様です。
カスタムコードを保存して有効にしたら、各アクションをテストしていき、成功したら応答スキーマを定義に登録します。

image.png

PowerAppsでの利用例

カスタムコネクタが完成すれば、後は普通にPowerAppsから呼んで利用するだけです。
以下はZaimに登録した出費一覧を表示するアプリの画面です。
image.png

PowerAutomateでも同様に利用できます。
カスタムコネクタでローコードの領域にもってくれば、後はこちらのものですね。
LINEと連携して家計簿に登録とかもできそうです。
image.png

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
0
Help us understand the problem. What are the problem?