ASP.NET Core のWebサイトに組み込みのOpenID ConnectスキームでAmazon Cognito User PoolsのログインUIを統合する

Amazon Cognito User Poolsのアプリ統合で、OpenID ConnectっぽいエンドポイントとログインUIが利用できる。 ASP.NET Core Authentication のOpenID Connectスキームは、ASP.NET Core メタパッケージに含まれているビルトインの機能。 Webサイトでajaxを使ったAPIアクセスではなくでブラウジングコンテキストでUser PoolsのIDにログインする。

services
  .AddAuthentication(options =>
  {
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
  })
  .AddCookie()
  .AddOpenIdConnect(options =>
  {
    options.ClientId = "client id";
    options.ClientSecret = "client secret";
    options.MetadataAddress = "https://cognito-idp.{Region}.amazonaws.com/{Pool ID}/.well-known/openid-configuration";
    options.Scope.Clear();
    options.Scope.Add("openid");
    options.ResponseType = "code";

    // Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectPostConfigureOptions
    options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
    options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OpenIdConnect handler");
    options.Backchannel.Timeout = options.BackchannelTimeout;
    options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB

    var byteArray = System.Text.Encoding.ASCII.GetBytes(options.ClientId + ":" + options.ClientSecret);
    options.Backchannel.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));

    options.ProtocolValidator.RequireNonce = false;
    options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.MetadataAddress, new AmazonUserPools_OpenIdConnectConfigurationRetriever(),
         new HttpDocumentRetriever(options.Backchannel) { RequireHttps = options.RequireHttpsMetadata });
    
    options.Events.OnRedirectToIdentityProviderForSignOut = (context) =>
    {
      context.ProtocolMessage.SetParameter("logout_uri", context.ProtocolMessage.PostLogoutRedirectUri);
      context.ProtocolMessage.SetParameter("post_logout_redirect_uri", null);
      context.ProtocolMessage.SetParameter("client_id", context.Options.ClientId);
      return Task.CompletedTask;
    };
  });
public sealed class AmazonUserPools_OpenIdConnectConfigurationRetriever : IConfigurationRetriever<OpenIdConnectConfiguration>
{
  Task<OpenIdConnectConfiguration> IConfigurationRetriever<OpenIdConnectConfiguration>.GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel)
  {
    return GetAsync(address, retriever, cancel);
  }
  static async Task<OpenIdConnectConfiguration> GetAsync(string address, IDocumentRetriever retriever, CancellationToken cancel)
  {
    var configuration = await OpenIdConnectConfigurationRetriever.GetAsync(address, retriever, cancel);
    var app_url = "https://<domain_prefix>.auth.<region>.amazoncognito.com";
    configuration.TokenEndpoint = app_url = "/oauth2/token";
    configuration.AuthorizationEndpoint = app_url + "/login";
    configuration.EndSessionEndpoint = app_url +  "/logout";
    return configuration;
  }
}

OpenID Provider Configuration

.well-known/openid-configuration で得られる情報が足りないので、IConfigurationRetrieverの実装で取得したOpenIdConnectConfigurationに不足するプロパティの値を設定した。

Backchannel HttpClient

エンドポイントへのアクセスはベーシック認証。Authorization ヘッダーでClient Secretを渡す必要がある。OpenIDConnectionOptionsのBackchannelのHttpClientで設定する。Backchannelは既定ではOpenIdConnectPostConfigureOptionsで構築される。既定のHttpClientにヘッダーを追加することはできなかったので、同じように作ってヘッダーを追加したHttpClientのインスタンスをOptionsに設定した。

おまけ

Razor PagesのPageModelではメソッドレベルのAuthorizeAttributeは効かない。

iPhoneからLANのPCへ名前でアクセスするために一時的にDNSサーバーを動かす

iPhoneのWebアプリでも作って勉強しようと思った。開発には短い間隔で書いて確認の反復が大事。同じネットワークにいるデスクトップPCのWebサーバーへiPhoneからアクセスしたい。カメラ使いたいのでHTTPSでアクセスする必要がある。自己署名証明書やオレオレ認証局だとiPhone側の設定とか面倒くさそう。Let's EncryptならiPhoneで信頼済みの証明書が簡単に手に入る。これを使いたい。

hostsファイルがあればいい。iPhoneにhostsはないらしい。iPhoneのネットワーク設定を見るとipv4DNSサーバーはルーターのアドレスになってる。ipv6も設定されてるけどよくわかってないから今回は無視する。ルーターはBUFFALO WXR-1900DHP3。ルーターでhostsみたいなことできるやつある。うちのルーターの設定画面にそれっぽい項目がない。

作業中だけDockerでDNSサーバー動かせば解決できる。dnsmasqというのが目的に適っていそう。

dnsmasqをインストールしたdockerイメージを作る

Dockerfile
FROM alpine

RUN apk update \
    && apk upgrade \
    && apk add --no-cache dnsmasq

ENTRYPOINT ["dnsmasq", "-k" ,"-u", "root"]

alpine linuxにdnsmasqをインストールするだけ。dnsmasqのオプションの-kは"keep-in-foreground"。これがないとコンテナがすぐ終了してしまう。dockerはENTRYPOINTのプロセスが終了したらコンテナを終了させる。

docker build
docker build -t dnsmasq - < Dockerfile 

何も持っていかないから'-'を指定する。Dockerfileだけ。今回は"dnsmasq"というイメージ名を付けた。

dnsmasqを起動する

docker run -d -p 53:53/tcp -p 53:53/udp --name dns dnsmasq -A /www.example.com/192.168.11.4

tcp:53とudp:53をDockerホストとつなぐ。dnsmasqの-Aオプションで解決したい名前とipを指定する。ここでの"dnsmasq"の文字はイメージの名前。ENTRYPOINTのコマンドの追加の引数として"-A"から後ろの文字が渡される。

Dockerホストのファイアウォール

tcp:53とudp:53の受信を許可する。

iPhoneの設定

  • 設定 - Wi-Fi - 接続しているネットワークをタップ
  • DNSを構成 - 手動を選択する
    • いまあるDNSサーバーを全部削除
    • DockerホストのIPを追加

あとかたづけ

docker rm -f dns

iphoneWi-Fi設定は「DNSを構成」を自動にして「リースを更新」で元に戻る。

docker build のWARNING

SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.

Docker for WindowsLinuxコンテナのbuildをすると必ず表示されるらしい。よくないことが起きたって意味ではなくて、よく確認してねって意味らしい。そもそも今回はpathに-を指定してるから関係ない。

Dockerで動かすASP.NET CoreとリバースプロキシをUnixドメインソケットでつないでみた

ASP.NET Coreの組み込みWebサーバーのKestrelはUNIXドメインソケット(UDS)で待ち受けができる、と知ったのでやってみた。

UNIXドメインソケットは同一ホストでプロセス間通信をするもの。リバースプロキシ(Webサーバー)とASP.NET Coreアプリケーションを同じホストで実行する。両者をDockerコンテナとして実行し、UNIXドメインソケットをDockerのボリューム機能で共有する。Webサーバーは触ってみたかったのでH2Oにした。

やってみた環境

> chcp 437
> systeminfo | findstr /B /C:"OS Name" /C:"OS Version"
OS Name:                   Microsoft Windows 10 Pro
OS Version:                10.0.16299 N/A Build 16299
> dotnet --version
2.1.2
> docker --version
Docker version 17.09.1-ce build 19e2cf6

Docker ImageはDocker Hubにある公式イメージを使った。(2017/12/13でのlatest)

ASP.NET Coreアプリケーション

素のASP.NET Coreアプリケーションを用意する

dotnet newコマンドの「web」テンプレートでHello Worldと返すだけのアプリケーションが作れる。アプリケーションの名前は「myapp」とする。

> mkdir myapp
> cd myapp
> dotnet new web

UNIXドメインソケットでの待ち受けを構成する

Program.csのBuildWebHostメソッドでASP.NET CoreのWebサーバー(Kestrel)がセットアップされる。WebHost.CreateDefaultBuilderの中でUseKestrel、UseIISIntegrationが呼ばれ、Kestrelの待ち受けが構成される。今回はCreateDefaultBuilderの後でさらに追加でUseKestrelを呼んでUNIXドメインソケットでのListenを追加することにした。UNIXドメインソケットのパスは「/tmp/myapp/kestrel.sock」にした。

// Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseKestrel(options =>
            {
                options.ListenUnixSocket("/tmp/myapp/kestrel.sock");
            })
            .UseStartup<Startup>()
            .Build();
}

公式ドキュメント: ASP.NET Core でのホスティング
WebHost.CreateDefaultBuilderのソースコード: MetaPackages/WebHost.cs at rel/2.0.0 · aspnet/MetaPackages · GitHub

ASP.NET Coreアプリケーションを実行する

dotnet publishコマンドでビルドする。docker runでアプリケーションを実行する。このときコンテナに2種類のボリュームをマウントする。一つはアプリケーションのdllを含んだホストディレクトリと対応したボリューム。もう一つはUNIXドメインソケットをコンテナ間で共有するためのホストディレクトリと対応しないボリューム。

> dotnet publish
> cd bin\Debug\netcoreapp2.0\publish
> docker run -d --rm -v /tmp/myapp -v %CD%:/app -w /app --name app microsoft/aspnetcore dotnet myapp.dll
docker run
--name app コンテナの名前をappとする
-v /tmp/myapp UNIXドメインソケットを作るディレクトリをボリュームとしてマウントする
-v %CD%:/app ビルド結果をフォルダごとDockerコンテナの/appディレクトリとしてマウントする
-w /app コンテナの作業ディレクトリを/appにする
dotnet myapp.dll コンテナ実行時のコマンドと引数

Docker run リファレンス docs.docker.jp
コンテナでデータを管理する

待ち受けを確認する

アプリケーションがUNIXドメインソケットで待ち受けしているか確認する。コンテナの中でcurlを使う。Hello Worldが返ってくればOK。

> docker exec -it app bash
# curl --unix-socket /tmp/myapp/kestrel.sock http://localhost/

curlでunix domain socket経由アクセスする
Can cURL send requests to sockets? - Super User
curl 7.50以降ではURLにホスト名を含めてよくなったらしい

リバースプロキシ

H2Oの設定ファイルを作る

yamlの設定ファイルを作る。ファイル名はh2o.confとする。接続を試したいだけなのでuser:rootとする。

hosts:
  default:
    listen:
      port: 8080
    paths:
      /:
        proxy.reverse.url: "http://[unix:/tmp/myapp/kestrel.sock]/"

access-log: /dev/stdout
error-log: /dev/stderr
user: root

公式ドキュメント: Reverse Proxy · h2o/h2o WikiProxy Directives - Configure - H2O

今回のやりかたでは、ASP.NET CoreはrootユーザーでUNIXドメインソケットを作る。H2Oもrootで実行しないとUNIXドメインソケットが読み書きできない。ちゃんと使おうと思ったらきちんとユーザーを作って設定しなければならないだろう。

H2Oを実行する

--volumes-fromで「app」コンテナのボリュームを共有する。 これで「/tmp/myapp」ディレクトリが2つのコンテナ間で共有される。 h2o.confファイルをコンテナの「/etc/h2o/」ディレクトリに置く。ファイル1つでもボリュームマウントできるが、今回はdocker cpを使ってファイルをコンテナにコピーした。

>docker create --rm --volumes-from app -p 8080:8080 --name h2o lkwg82/h2o-http2-server
>docker cp h2o.conf app:/etc/h2o/h2o.conf
>docker start h2o

できあがり

Dockerホスト1のブラウザでhttp://localhost:8080にアクセスするとアプリケーションからの応答が返ってくる。

手軽にできるようにDocker HubのイメージのDockerfileを見てdocker runのオプションを決めた。docker build のコンテキストと、DockerfileでのCOPYがこのイメージの肝。DockerfileはDockerデーモンが実行するので、docker buildしたクライアントとは見えるファイルが違う。そのためのコンテキスト。

参考にした: Dockerfileを書く時の注意とかコツとかハックとか | kim hirokuni

おまけ1

H2Oのコンテナの中でUNIXドメインソケットへアクセスできているか確認する。このイメージはalpine linuxがベースになっている。bashがないので、コマンドashで対話する。パッケージマネージャーはapkupdateしてaddでインストール。

> docker exec -it h2o ash
# apk update
# apk add curl
# curl --unix-socket /tmp/myapp/kestrel.sock http://localhost/

おまけ2

Kestrelの既定の待ち受けポートはTCP:5000。UseIISIntegration(IIS統合)が有効になるとKestrelの待ち受けポートが動的に決定され、IIS経由のリクエスト以外は拒否される。しかし、後で追加するUnixドメインソケットでの待ち受けには影響がないっぽい。

実行中のASP.NET CoreアプリケーションでWebサーバーの待ち受けがどうなっているのかIServerAddressesFeatureでわかる。

Kestrel web server implementation in ASP.NET Core | Microsoft Docs

 var serverAddressesFeature = app.ServerFeatures.Get<IServerAddressesFeature>();

片付け

> docker rm -v -f h2o app

  1. 今回はDocker for WindowsなのでWindowsからブラウザでhttp://localhost:8080でDockerで動くH2Oにアクセスできる

WebサーバーなしでLet's Encryptに証明書を発行してもらった

Webサーバーはまだない、とにかくLet's Encryptの証明書をとってみたかった。Route53にドメインはある。自分のWindowsパソコンを使う。インターネットからアクセスできるサーバーを用意したくない。

Dockerでcertbotを使ってDNS-01方式で証明書を発行してもらう。

  • Let's EncryptはCA(認証局)
  • Let's EncryptはACME(Automatic Certificate Management Environment) protocolで証明書を発行してくれる

  • certbotはACME protocolのクライアント

  • certbotは電子フロンティア財団(eff)が作ってる
  • Docker Hubにイメージがあるcertbot/certbot

  • ACMEにはDNS-01方式がある

  • DNS-01仕様はDNSのTXTレコードでドメインの所有を示せるので、Webサーバーを用意しなくてもいいい

  • Docker for Windows

docker run -it --rm \
    -v C:\Users\Public\etc\letsencrypt:/etc/letsencrypt \
        certbot/certbot certonly  \
            --manual \
            --preferred-challenges dns \
            --email "email address" \
            --domain "domain"
  • いくつか確認の入力がある
  • トークンが発行されるので、対象ドメインサブドメイン _acme-challenge.[domain]のtxtレコードに設定する
  • txtレコードの確認方法
  • nslookup -q=TXT [domain'
  • 証明書は /etc/letsencryptフォルダのlive/$domainの中にある

  • certbotには、Route53のtxtレコードの設定まで自動でやってくれるプラグインもある

20171112 「豊田市IT勉強会 第3会 もくもく会」へ行ってきた

この記事はScrapboxのページのスナップショットです1


豊田市IT勉強会 第3回 もくもく会 - connpass

会場
  • http://mg-toyota.com/
  • マイカフェなの?マイカフェではないの?
  • 静かだった
  • ビジター 9:00-17:00 1000円 飲みものあり
  • 自宅から車で1時間かからないくらいで行けた
    • 道路工事で片側交互通行が2箇所とイケア渋滞があった
  • 駐車場はすぐ近くに1日打ち切り1000円ぐらいのが一つだけあったが満車だった
    • 打ち切りなしの駐車場が周囲にたくさんある 30分150円 (8h 2400円)
    • 愛知環状鉄道の高架下が一日打ち切り1000円くらいである
      • 隣り合った同じブランドのパーキングで駅から遠い方は少し安い 800円
他の人がやってたこと
  • プレゼン点数判定
    • 言い淀み認識
    • 行動力って大事だなと思った
  • ゼロから作るDeep Learning ISBN 978-4873117584
    • 自分も同じ本を読んで意見を聞いたりできるかも
      • 一人で読むと細かい表現に悩むことが多いので
  • ChromeExtension
  • java?web?入門本
    • 本の通りにならないと言っていた
      • 一緒に読んでみるのを提案してもよかったかも
        • javaはわからなくても本に書かれていること読むことはできるから
  • 形態素解析
    • 行動力って大事だなと思った
自分がやったこと
  • docs.microsoft.com/en-usと /ja-jpを行ったり来たりするブックマークレットを作ろうと思った
  • 最近jsよくわかってないです。jQuery登場以前にフロントエンドを仕事で少し触った程度の経験
    • ブックマークレット アドレスjavascript:に書いたjsを実行させるやつ
      • chromeを常用。とりあえずスクリプトの長さは気にしなくてもよさそうな雰囲気をインターネットから察知した。
    • とりあえずchromeデベロッパーツールのコンソールで素朴にページ遷移させるコードを書いてみる
    • 開発者向けのWeb技術 | MDNを参照しながら
      • まず、ブラウザjsにはwindowというのがグローバルオブジェクトとして定義されている、なのでwindowと書かずにwindowのプロパティにアクセスできる
      • window.locationプロパティがある。
        • getは不変のLocationオブジェクト、setは文字列かLocationオブジェクト
      • Location.replaceメソッドを使えば履歴が増えないから進む戻るに影響を与えない
      • string.replaceで正規表現を使った置換
      • javascript
        • location.replace(location.href.replace("/en-us/", "/ja-jp"))
      • これだと片道。行ったり来たりしたい
        • ["/en-us/", "/js-jp/"]
        • MDNもjaen-USがある
    • というか対応する2つのページが同じドメインでないかもしれない、同じパス位置の変更で対応できないかもしれない
      • こんな感じかなのコードを考えた。長いのでページ末尾に置いた。
      • そもそも、ブラウザのタブを2つ切り替えればいいんじゃないか
      • chrome Extensionだ
        • appというのは過去あったけどそれとは別のもの
        • plugin、addon、addinはchrome用語には無いっぽい
        • デベロッパーモード、パッケージ化されていない拡張を読み込む
        • manifest.json
      • V.S.Codeでtypescript
        • 補完とリアルタイムのコンパイルエラー表示があるといいな
        • ショートカットキーの`Ctrl+``がwindowsの言語選択が強くて使えない
        • npmでのプロジェクト管理
        • gitignoreをgithub gitignoreからダウンロード
        • 雑に調べてコピペして実行
          • これが行動力か
    • なんかついでにWSLで諸コマンド使えるようにしとこうと思った
      • 使うのはubuntu。Store版をインストール済み
        • >wslでセッションに入ってコマンドをうつ
          • wsl.exeではcurrentDirectoryでセッションが開始される。たしか以前は、ubuntu.exeとかアプリ名で開始しないとcurrentDirectoryにならない挙動だったはず。また挙動が変更されるかもね
        • >wsl コマンドで都度wslにコマンドを実行させてもいい
        • nmpもwslのものを使おうとおもった
          • インストールから手探り。雑に調べてコピペして実行。バージョンが新しくない。雑に調べてコピペして実行。なんだかよくわかってないままにnodeとnmpが最新版になった
            • これが行動力か
    • ここまでやってgithubリポジトリ作った
思ったこと、連想したこと、ぼんやり浮んだイメージ
  • google検索 「音声認識 言い淀み」
  • DeepLearningは機械学習の手法の一つなの?
    • パターンを発見する = 概念を作る = 抽象化 と言うことは可能か?
      • ちゃんと調べておくこと
  • もくもく会の参加者ステータスがモニターできると楽しそう
    • その人の様子は見ればわかるけど、PCの中でどんな様子かまでは分かりづらい
    • 明示的にアテンションを求めなくても周囲にPCでの作業内容の雰囲気が伝わるとコミュニケーションのきっかけになるのでは
    • グループチャットでそれぞれ自分の文脈で独り言
    • ヒートマップ
      • スクリーンキャスト
        • 画像認識。画面上の文字を認識するのではなくて、PCと人との対話の雰囲気を可視化
      • アイトラッキング
      • キーボード、マウス操作の熱さ
      • PCのネットワークアクセス傾向
      • 黒い画面のコマンドの成否
      • イスの座面圧力、足と床の圧力
        • 立った座った、足を組みかえた、前のめりになった、のけぞった、足踏みした
  • 画像認識するとして妄想したこと
    • カーテンモード
    • ネットーワークに流すのは忌避感を持つのではないか
      • HDMI端子に挿す装置
      • 画像を統計情報に変換する
    • 自分のものでない装置への信頼性
      • シールされたハードウェア
        • 信頼できる誰かによって作られ手元にとどくまでに変えられていないか
      • インストールされたソフトウェア
        • ソフトウェア自体に悪意が含まれていないか
        • インストールされたものが本物か
  • 今日知った言葉
    • 正真性 :integrity 完全性
    • 真正性 :authenticity

疑似コード

//変換関数を適用するかの判定
delegate bool Match(Location);
delegate UrlString Transformer(Location);

// docs.microsoftとかMDNみたいなパターンのルールを作る
(Match, Transformer) MakeBasicRule(string urlPrefix, string pathSegment1, string pathSegment2)
{
    // urlの先頭から一致
    var match = location => location.href.StartsWith(urlPrefix);
 
    // パスの同じ位置で入れ替え
    var transformer = location => {
        var url = location.href.replace(pathSegment1, pathSegment2);
        if( url != location.href){
            return url
        }
        return location.href.replace(pathSegment2, pathSegment1);
    };

    return (match, transformer);
}

basicRule = [
    ["https://docs.microsoft.com/", "/en-us/", "/js-jp"],
    ["https://developer.mozilla.org/", "/en-US/", "/ja/"]
];
 
string Toggle(Location location){
    var ruleSet = basicRule.Select( rule => MakeBasicRule(rule[0], rule[1], rule[2])).ToArray();
 
    // matchするルールがなければnull
    return ruleSet.FirstOrDefault( rule => rule.Item1(location))Select(rule => rule.Item2(location));
}

  1. daiiz/ScrapboxコンテンツをMarkdownに変換するBookmarkletを使った後に手動で体裁を修正して掲載しています。

VisualStudioでWebApp開発中にCtrl+F5を押して素のChromeを起動させる

f:id:azechi_n:20171109150238p:plain
デバッグの開始、デバッグなしで開始
Visual Studio 2017でデバッガをアタッチせずにWebアプリケーションを実行するにはCtrl+F5(「デバッグの開始」のショートカットキー)を押す。すると、ブラウザが開いてアプリケーションのアドレスにアクセスする。何も設定していない状態だとWindowsの既定のブラウザが対象になる。既定のブラウザをchromeにしているので普段使っている状態のchromeが開く。

これで別に不都合はないのだけれど、「デバッグの開始」をした場合はchromeがブックマークとかプロファイルとか無いプレーンな状態で開く。どうせなら「デバッグなしで開始」のときにもプレーンなchromeが立ち上がって欲しい。

f:id:azechi_n:20171109150239p:plain
素のchrome

デバッグの開始」のときのchromeの起動オプションをchorome://versionで調べた。

f:id:azechi_n:20171109150941p:plain
chrome起動時のコマンドライン

--user-data-dirを指定して別のユーザーデータで起動すればいいらしい。フォルダが存在しない場合は起動時に作られる。

f:id:azechi_n:20171109150235p:plain
ブラウザーの選択

ブラウザーの選択」からブラウザーの追加ができるので--user-data-dirを引数にしたchromeを追加する。