Python Flask のアプリへ Windows 認証ユーザを連携したい

# はじめに
[前回の検証](https://qiita.com/mako1047/items/48d4548f8989c24ef36b)中に、 IIS で Windows 認証を有効にして Python アプリに認証ユーザを引き渡せたら今後色々と楽でいいのに、と思ったので、調査・検証してみました。

# 対応方法(概要)
HttpPlatformHandler v1.2 では、Windows 認証トークンを HTTP Header で受け取れるとのことで、その値を Python.NET を活用して処理するようにします。
(下記表最終行に、forwardWindowsAuthToken=true にすることで、HTTP Header として X-IIS-WindowsAuthToken が得られる、CloseHandle は受け取った側で責任持って実施すること、との記載あり)

https://learn.microsoft.com/ja-jp/iis/extensions/httpplatformhandler/httpplatformhandler-configuration-reference#configuration-attributes

# 開発環境
Visual Studio 2022 で .NET Framework 向けに開発した方が良かったのですが、事情により、Visual Studio Code + .NET 8 です。

[インストール物]
Visual Studio Code
.NET 8 SDK
VC# Dev Kit (VS Code Extension)

# 事前調査

Windows 認証機能はオプションなので、未インストールであればサーバーマネージャーからインストールしておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/dc3bd079-0bfd-f984-c151-edebadce9d9d.png)

対象サイトに対して Windows 認証を有効に、匿名認証を無効にする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/916c8f24-7ead-a2a8-986d-3fd6eaf6b375.png)

前回開発物のログを強化し、HTTP Header を出力するようにしてみた。

```python_iis_w.py
from flask import Flask, request

+ import logging
+ logger = logging.getLogger(__name__)
+ logger.setLevel(logging.DEBUG)
+ fh = logging.FileHandler("C:\\home\\LogFiles\\test.log")
+ fh.setLevel(logging.DEBUG)
+ fh.setFormatter(logging.Formatter("%(asctime)s : %(message)s"))
+ logger.addHandler(fh)

app = Flask(__name__)

@app.route("/")
def hello_world():
  + logger.debug("----------")
  + logger.debug(request.headers)
    return "<p>Hello World! with Waitress1</p>"

if __name__ == '__main__':
    app.run(debug=True)
```

Web.config で当機能をONにして動作確認。

```Web.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="PythonHandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified"/>
    </handlers>
    <httpPlatform processPath="C:\WebSites\HelloWorldPyW1\venv\Scripts\python.exe"
                  arguments="waitress_server.py %HTTP_PLATFORM_PORT%"
                  stdoutLogEnabled="true"
                  stdoutLogFile="c:\home\LogFiles\pythong.log"
                  startupTimeLimit="60"
                  processesPerApplication="16"
                + forwardWindowsAuthToken="true" >
      <environmentVariables>
      </environmentVariables>
    </httpPlatform>
  </system.webServer>
</configuration>
```

連携されている↓

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/ed0b009e-98b5-4a71-6fda-c5e21248e626.png)

が、これが認証トークンへのハンドルであり、CloseHandle しろということなので、いきなりアンマネージリソースをリークさせてます(笑)　よろしくないです。

# 開発(DLL)
空のフォルダ(C:\PythonPJ\GetUserInfoCS とした)を用意して、HTTP ヘッダの値をパラメータとして受け取り、ユーザー名を返却する C# DLL を .NET 8 で開発する。

```
cd C:\PythonPJ\GetUserInfoCS
dotnet new classlib
```

参考ソースは下記、依存モジュールを減らしたかったので適宜調整。

https://github.com/aspnet/IISIntegration/tree/master/src/Microsoft.AspNetCore.Server.IISIntegration

```TokenConverter.cs
using System;
using System.Security.Claims;
using System.Globalization;
using System.Security.Principal;
using System.Threading.Tasks;
using System.Runtime.InteropServices;

namespace GetUserInfoCS {

    public class TokenConverter
    {
        internal static class NativeMethods
        {
            [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
            public static extern bool CloseHandle(IntPtr handle);
        }

        public string GetUser(string xtoken)
        {
            var ret = "";

            var tokenHeader = xtoken;

            int hexHandle;
            if (!string.IsNullOrEmpty(tokenHeader)
                && int.TryParse(tokenHeader, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out hexHandle))
            {
                // Always create the identity if the handle exists, we need to dispose it so it does not leak.
                var handle = new IntPtr(hexHandle);
                using (WindowsIdentity winIdentity = new WindowsIdentity(handle))
                {
                    ret = winIdentity.Name;
                    NativeMethods.CloseHandle(handle);
                }
            }

            return ret;
        }

    }
}
```

ビルドすると、マルチプラットフォーム対応の .NET なのに Windows 固有の実装をしている旨、警告がでるが無視する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/ba7320c9-dc78-26b9-4308-8c2b5a3466ea.png)

できたファイル(GetUserInfoCS.dll)は python_iis_w.py と同じパスにコピーする。

# Python ソースへの組み込み

Python.NET を組み込んで改訂する。

```
.\env\Scripts\Activate
pip install pythonnet
```

毎回確実にリソースリークしないよう対応する必要があり、Flask の before_request で処理するのがよさそう。

```python_iis_w.py
from flask import Flask, request

+ from pythonnet import load
+ load('coreclr')
+ import clr
+ clr.AddReference('GetUserInfoCS')
+ from GetUserInfoCS import TokenConverter

import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler("C:\\home\\LogFiles\\test.log")
fh.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter("%(asctime)s : %(message)s"))
logger.addHandler(fh)

app = Flask(__name__)

+ @app.before_request
+ def before_request():
+     logger.debug("----------")
+     logger.info(request.headers)
+     xtoken = request.headers.get("X-IIS-WindowsAuthToken")
+     if xtoken:
+         tc = TokenConverter()
+         uid = tc.GetUser(xtoken)
+         logger.info(uid)

@app.route("/")
def hello_world():
-   logger.debug("----------")
-   logger.info(request.headers)
    return "<p>Hello World! with Waitress1</p>"

if __name__ == '__main__':
    app.run(debug=True)
```

# 動作確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665941/a41badbe-00aa-1418-ffc1-51b5b5dff2d6.png)

ドメインID + "\\" + ユーザID 形式で取得できたので、ログインユーザをもとにアプリ内の権限レベルをチェックする等、好きにできそうです。

# 最後に
[参考にさせていただいた主情報]
ほぼこちらの記事が答えでした、ありがとうございます。
https://blog.shibayan.jp/entry/20151029/1446126178