はまったりひらめいたり…とか…

Angularや.NETやAzureやその他色々。

AADのアプリロール情報を利用してWebAPI(ASP.NET Core)やWebアプリ(Angular)の動作を制御する

はじめに

この記事は2022/11/5時点の情報で記載されいます。

記事中のコードや情報については参照時期によっては正常に動作しない可能性がありますのでご留意ください。

コードは下記のライブラリのバージョンで動作しています。

  • Angular: ^14.2.0
  • @azure/msal-browser: ^2:30.0
  • .NET Core: 6.0

AADのアプリロールで制御を行いたい

AADを利用し社内アプリケーションなどの認証が容易になります。

認証ときたら認可が必要でADアプリに対して「APIのアクセス許可」を使うのが一般的かもしれませんが、「アプリロール」も利用することが可能です。

アプリロールはアプリケーションだけでなくユーザーやグループに対しても割り当てることが可能なので、「APIのアクセス許可」に追加する情報として利用することで下記のようなより細やかな制御を行うことが可能となります。

  • ex1)アプリケーション自体にアクセス可能だがこのページはアクセス不可
  • ex2)WebAPIリソース自体にアクセス可能だが特定WebAPIだけ実行不可

AADアプリへのロール付与とロールへのユーザー割当

AADアプリの「アプリロール」から設定を行います。

今回は許可されたメンバーの種類「ユーザーやグループ」でReaderWriterのロールを作成してみました。

次に作成したロールにユーザーやグループを割り当てます。数赤枠箇所からエンタープライズアプリケーションの画面に移動します。

「ユーザーまたはグループの追加」からユーザーを追加することができます。

この際に、先ほど作成したロールを選択できるようになっています。

認証を行うと下記のようにJWTトークンにロール情報が付与されるようになります。

WebAPIの承認の制御

WebAPIの承認時にアプリロールの検証を行うように変更します。

といっても、Authorize属性にRoles情報を付与するだけです。簡単ですね。

[Authorize(Roles = "App.Writer")]
[ApiController]
[Route("[controller]")]
public class WriterController : ControllerBase
{
    ...
}

上図のアクセストークンを利用してみます。

結果は403となりました。

ASP.NET Core WebAPIで設定したRolesをRoles = "App.Reader"に変更します。

今度は通信が成功します。返却されたトークンに含まれるアプリロール情報を参照し制御できていることが確認できます。

[Authorize(Roles = "App.Reader,App.Writer")]のように複数のロールを指定することも可能です。

Webアプリの動作制御

そこまで難しい話ではなく先程見た通りアプリロールがJWTトークン上に含まれていることが確認できました。そのアプリロールを参照し制御を入れてあげればいいだけです。

ちょっと凝ったことをしたいのでAngularを使用していますが、認証認可ライブラリのmsalはバニラでも使える@azure/msal-browserを使用していますし、他のフレームワークでもアプリロールの参照の仕方部分はそこまで変化はないと思います。

ロール情報の取得

msalのidTokenClaimsroles: string[]として格納されいます。

ただし、idTokenClaimsはObject型ですし、この情報は抜き取って利用しやすい形にしておきます。

this.client = new PublicClientApplication(environment.msalConfig);
const res = await this.client.handleRedirectPromise();
if (!res) {
  this.client.loginRedirect();
  return;
}
this._myRole = (res.idTokenClaims as { roles: string[] }).roles;

ロールによる遷移の制御

Angularの場合、CanActivateなどでルーティングに対するアクセス制御が可能です。

@Injectable({
  providedIn: 'root'
})
export class AuthGuardService implements CanActivate {

  constructor(
    private authService: AppService,
    private router: Router
  ) { }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    const url = state.url;
    const myroles = this.authService.myRole;
    let ret = true;
    if (url.includes('reader')) {
      ret =  myroles.includes('App.Reader');
    }
    if (!ret) {
      this.router.navigate(['access-deny'])
    }
    return ret;
  }
}

雑に書くとこんな感じです。格納しているアプリロールを参照して遷移可能かどうか判定しているだけです。

アクセス不可なルートにアクセスしようとしている場合はDenyページなどに飛ばしています。

WebAPIへの通信に関しては生成されたアクセストークンにアプリロール情報が含まれていることが確認できたのでacquireTokenSilentなどを使用して取得できたアクセストークンをそのまま使えば問題ありません。

最後に

フロント、バックエンドに大きな変更やライブラリの追加をすることなく、アプリ内のロール制御が行えることが確認できました。

多くの場合はビジネスロジックに引っ張られたり、AADへのアクセス権限、管理をシステム部門などが容易にしたいという観点からロール部分の実装は自前で行う選択肢が取られることが多いかと思いますが

そこまで厳しい要件がないのであれば、このアプリロールを利用してアプリ内の権限制御を簡単に実装するのもありかと思います。

今回実験で使用したデモは下記に格納しています。

github.com

参考

アプリ ロールを追加してトークンから取得する - Microsoft Entra | Microsoft Learn

OpenHack DevOpsに参加しました

最近OpenHackのDevOpsカテゴリに参加しましたのでそのレポートとなります。

OpenHack DevOpsとは

openhack.microsoft.com

OpenHackについては上記の公式ページに記載されています。

AIやContainerといくつかのカテゴリが存在するのですが、今回はMicrosoftの方からご紹介いただいたDevOpsのOpenHackに参加しました。

OpenHack DevOpsについては下記のように記載されています。

DevOps OpenHack enables sound DevOps fundamental upskilling and developing Zero-downtime deployment strategies, which translates to reduced friction in production deployments, ensures that deployments of new features can occur more frequently and safely without requiring system downtime.

(DevOps OpenHackは、健全なDevOpsの根本的なスキルアップとゼロダウンタイム展開戦略の開発を可能にし、本番展開での摩擦を減らし、システムのダウンタイムを必要とせずに新機能の展開をより頻繁かつ安全に行うことができます。)

必要な技術要件は下記の通り記載されています。

GitHub or Azure DevOps (team choice), Azure App Service, Log Analytics, Application Insights, Azure Monitor, Azure SQL Database, Azure Container Registry, Key Vault, Bicep, Terraform

OpenHack DevOps参加レポート

参加形態について

今回は会社のメンバーとチームとして参加しました。ソロでの参加も問題ないようでその場合は2~3人(最大5人)からなるチームになるように編成されるようです。

今回はちょっと無理言って会社で参加する全員(5人)を1チームにしてもらいました。チーム編成まわりのわがままは結構聞いていただけるようです。

知識を得る、できることを増やす、自分がどこまでできるかチャレンジするというベースであればソロでインスタントなチーム内で活躍するのが全然いいと思うのですが、会社のチームで参加するほうが色々得るものは多いと思いました。

今回はワイガヤになると予想していたので、会社の会議室を使ってそこに集まるという方式をとり(他のメンバーからするとずるい)、案の定議論が活発にされたので会議室をとったのは良い判断だったと思います。

デメリットがあるとするならば普段会社で一緒に仕事をしているメンバーなので

要素技術の話で横道にそれて業務に落とし込んだ話になったり、ガチ運用を考える方向に行ったりとしがちだったことですね。

(課題を片付けていくという目的に対してはデメリットなのですが個人的にはメリット寄りの内容ではあります。)

用意しなければいけないもの

特にありません。ありとあらゆる環境は用意していただけるので、なにかの契約をして置かなければいけない。などの制約は存在しませんでした。

ただ、Microsoftの方の紹介で参加したので会社で関わりのあるMicrosoft社員の方や、Twitter等で観測できるMicrosoftの社員の方に相談してみるのがいいと思います。

参加に必要な技術レベル

問題内容はNDAなので詳細にはかけませんが、まったくのプログラム初心者やAzureを使用するのでAzureの初心者が受けるには厳しい内容だとは思います。

ただ、詰まってたらコーチが助言してくれたりもしてくれるのでDevOpsで必要なCICDの知識や、BicepやTerraformといったIaCの知識がバッチリないといけないかというとそうでもなさそうな印象です。

(実際ウチのチームはIaCの実践とかは皆無でTerraformもかじったくらいの知識を持ってる人が2人いるくらいだった)

少なくとも自動化のイメージができるという意味では、ソースコードのビルドからデプロイまでの工程をコマンドで実行できるとかのレベルがあれば十分かもしれません。

デプロイする対象のコードはC#からnode.js、Javaといろいろ揃っているのですが、普段自分たちが触っていない言語が出てくると面食らうと思うので、それくらいであれば一回MSの方に聞いてもいいかもしれません。

参加した感想

参加するまでは難しそうという不安がありはしたのですが、参加してみると非常に楽しいものでした。

DevOpsの課題は実際の業務にありがちな問題に対する解決というものだったので、自分たちの業務に落とし込むことも容易でしたし活かせるものは非常に多かったです。

こういった研修(?)に会社で参加するとなると、代表者が参加して持ち帰って共有して…といった流れになりがちかもしれませんが

会社のチームで参加すると議論も深まってより良いものになると感じますので、ぜひチームで参加することも検討してみてください。

ある程度課題を片付けるとデジタルバッチも貰えます(貰えた。良かった。)

www.credly.com

Azure Communication Services Emailを触ってみた話

はじめに

2022年5月25日時点の記事となります。

この時点でAzure Communication Services EmailはPreviewです。

記事中にでてくる画面キャプチャやコードは参照時期によっては変更されている可能性がありますのでご留意ください。

Azure Communication Services Email

Azure上でメール送信する仕組みは非推奨ということもあり

Azureでメールを取り扱いたいといった場合、Logic Appsを経由してOutlookで送るとか、外部サービスのSend Grid等を使用するかしかありませんでした。

Azure 上にメールサーバー/SMTP サーバーを構築する場合の注意事項 | Microsoft Docs

しかし、今回のBuildでAzure Communication Services Emailが発表されたことにより

外部サービスを使うことなく利用できることになりそうなので良きって感じですね!

早速見ていきたいと思います。

触ってみる

Communication Servicesを作成する

Communication Servicesを介してEmailが送信されるのでまずはCommunication Servicesの作成からです。

Data Locationは一応United Statesを選択。

Communication Services Emailを作成する

作成時の設定内容は特に難しいものはないので割愛

Domainの設定を行う

まずは雑にAzureのドメインを利用して作ってみます。

といってもClickしたら作られるだけなので特に設定作業ははさみません。簡単。

Communication ServicesとCommunication Services Emailを接続する

Communication Services側からEmailのDomainで先程作成したドメインと接続を行います。

接続もリソースを選択していくだけなので簡単ですね。

送信コードを組んでみる

今回はC#で組んでみようと思います。

NuGetのPrereleaseでAzure.Communication.Emailが提供されているのでそれを利用します。

接続文字列はCommunication Services側のKeysから取得します。

あとはこんな感じで送信。ほぼ公式のサンプル通りですがちょいちょい付け足しています、

using Azure.Communication.Email;
using Azure.Communication.Email.Models;

Console.WriteLine("Hello, World!");

var connectionString = @"Communication Servicesから取得した接続文字列";

var emailClient = new EmailClient(connectionString);

// EmailContentの文字列引数はメールタイトル
EmailContent emailContent = new EmailContent("Welcome to Azure Communication Services Email APIs.");

// 内容(Htmlが設定されたらHTML側が優先される?)
emailContent.PlainText = "This email meessage is sent from Azure Communication Services Email using .NET SDK.";
emailContent.Html = "<h1>HTMLのあれやこれや</h1>";

// 送信するアドレスの設定
List<EmailAddress> emailAddresses = new List<EmailAddress> { new EmailAddress("<送信先メールアドレス>") };
// EmailRecipientsのインスタンスは(To, Cc, Bcc)引数のようだ
EmailRecipients emailRecipients = new EmailRecipients(emailAddresses);

EmailMessage emailMessage = new EmailMessage("<送信元メールアドレス>", emailContent, emailRecipients);
SendEmailResult emailResult = emailClient.Send(emailMessage, CancellationToken.None);

// 送信結果の確認
var status = emailClient.GetSendStatus(emailResult.MessageId);

Console.WriteLine("おわり");

こんな感じで届きました。簡単ですね!

送信までは少し時間が掛かるようなので送信結果を抑えてから別の処理を行いたい場合は

emailClient.GetSendStatus(emailResult.MessageId)outForDeliveryになるまで待ちましょう。

ステータスはこんな感じみたいです。

添付ファイルはBase64変換したファイルをEmailAttachmentに設定してEmailMessageAttachmenstsのListに追加すれば良さそうです。

// 添付ファイル
string base64St = Convert.ToBase64String(File.ReadAllBytes(@"C:\temp\sample.txt"));
EmailAttachment attachment =  new EmailAttachment("さんぷる.txt", EmailAttachmentType.Txt, base64St);
emailMessage.Attachments.Add(attachment);

上記のコードで👇のように届きます。

MS Docsより詳細なサンプルも例のごとくGitHubに転がっているので何はともあれ見てみるのがいいと思います

気になること

まだベストプラクティスは出ていないですが、他のAzure SDKのClient同様RESTベースの通信していそうです。

ライブラリの実装:azure-sdk-for-net/EmailRestClient.cs at 7a5012587cee93bf34d23cc8caddb0dd915fba1c · Azure/azure-sdk-for-net · GitHub

SDKのプラクティス:C#.NET Guidelines: Implementation | Azure SDKs

なのでWeb Appsなどで構成する場合はSingleton構成にするのがベターなのかなーと思ってたりはしています。

送信元の情報変更

メールアドレスなど送信者の情報のメタデータを変更できます。

作成したメールドメインの設定画面から変更できるようです。

MailFromはDoNoyReplyから好きなものに変更できます

ユーザーがメールを開いたかなどのトラッキングも行えるようです。

設定を行ったあとメール送信すると下図のようになります。

名前やアドレスはもちろん、トラッキング用と思しきタグが生えているのも確認できます。

データの確認なんかはまだログだとできないんですかね。

他のサービスよろしくACS???みたいな感じで生えているかなーと少し期待していたのですが。ここもしばらく待っておけば出てくるかもしれません。

(EventLog追っかける元気はなかった…😫

価格

Public Preview段階ですが価格情報ももう出ています。

Email pricing - An Azure Communication Services concept document | Microsoft Docs

電子メール送信と転送されたデータで価格が決定するようです。

価格の例が出ているのでわかりやすいですね。

まとめ

Azure Communication Service Emailでメールの送信を行うことが確認できました。

色々とやりにくかった、Azureでのメール送信が楽になるかもしれませんね。

SendGridなどの外部サービスとくらべて何ができて何ができないかは気になるところではありますが、Azureの中で一つの選択肢ができたのはとてもわくわくすることだと思います。

Emailを使用するような機能を実装したい場合は積極的に検証していきたいですね(まだPreviewですが…

余談ですが、会社でSend Gridを使っているのですが、それと同じようなノリでSDKを使うことができました。

ロジックをうまく分離できていたらメール送信層だけすげ替えればあっさりと移行できるかもしれないのも良いなと思いました。

かんたんなdevcontainerのコンテナ構成作り方メモ

はじめに

この記事は2022年3月時点の情報で作成されています。

参照時期によっては掲載のコードなど動作しない可能性がありますのでその点ご留意ください。

とても便利なdevcontainer...だが…

Visual Studio Codeのdevcontainer(Visual Studio Code Remote - Containers)は非常に便利です。

が、世間一般で公開されているコンテナだけだと、ちょっと自分の開発用途に合わないってことが多いです。

なので基本的に自分でコンテナイメージを作ることになると思うのですが、これがまぁ大変です。

常日頃Dockerfileを弄るような開発をしているわけではないですし、一回作ったらあとはそれ使い続けるみたいなこともするのでなかなか食指が伸びない。

(.NETの開発なんかだと構築比較的楽だから余計に伸びない…)

なので、都度都度思い出せるように楽に作るためのメモ書きを残しておこうと思います。

devcontainerの作り方自体は記事が多く転がっていると思うので開発環境のコンテナづくりのメモになります。

devcontainerのDockerfileやdocker-composeファイルを楽に作る

そもそも自分の開発用途に合うものがないか探してみる

vscode-dev-containersのリポジトリに大量のサンプルが転がっています。

そもそもここの構成で自分の開発用途にあっているものがないか探してみるというのが良さそうです。

vscode-dev-containers/containers at main · microsoft/vscode-dev-containers · GitHub

.NETの開発からJava、Denoまで様々なものが転がっています。

ひょっとしたらここ覗くだけで解決する可能性のほうが高いかもしれません。

コンテナの作りかたを見る

たとえばdotnetAzure Functions(Java)を組み合わせたいなーとなった場合があるかもしれません。

その場合はそれぞれの.devcontainerの中にあるDockerfilebase.Dockerfileの中を見てみます。

Azure Functions(Java)の場合はDockerFile内にFROM mcr.microsoft.com/azure-functions/java:4-java11-core-toolsしているのですが

そもそもこいつどうやって作ってるのよ?って情報はコメントで書いてあるURLのhttps://github.com/Azure/azure-functions-docker/blob/dev/host/4/bullseye/amd64/java/java11/java11-core-tools.Dockerfileから参照することができます。

dotnetの場合はbase.Dockerfileですね。

このような感じで比較的シンプルな構成から開発環境を構築するための構成を、どんどんたどっていくこともやりやすいです。

なのでまずはこのリポジトリにあるものをベースに始めていくというのが良さそうだと思っていますし

開発用のコンテナを作るための初学者の勉強にも最適だと思います。

作ってみたもの

CsharpStudy/LearnDevContainer at master · Takas0522/CsharpStudy · GitHub

Angular + ASP.NET Core + Azure Functions(Java) + azurite な完全に俺得にしかならないような環境…

ただ、ここで書いている流れでやれば、こういうニッチな環境であってもコンテナ初学者でも構成しやすいだろうと思いました。

YARPを利用して実装したWeb APIをAzure ADで保護してOn-Behalf-Ofフローでトークンを再取得し通信する

はじめに

この記事は2021年11月に記述された内容です。

参照時期によっては記載している実装内容と異なっている可能性がありますのでその点ご留意ください。

記事中の実装内容はYarp.ReverseProxyの1.0.0をベースに実装されています。

YARP

Visual Studio 2022 Launch 記念 C# Tokyo イベントの発表中にYARPがリリースされたことが触れられていました。

触ってみたところ、欲しかったものだったので公式Docsに記載されている内容以外で少し検証したことを軽くまとめておこうと思います。

そもそもYARPとは

Yet Another Reverse ProxyでYARPらしいです。

名前の通りリバースプロキシで、ASP.NET Core上でプロキシルーティングや処理動作を簡易に使用したりカスタマイズしたりできるライブラリのようです。

具体的には下記のようにappsetting.json内に記述することでhttp://example.com/proxyone/proxyapiにアクセスするとhttp://other.example.com/proxyapiにアクセスしデータが取得されます。

{
...
  "ReverseProxy": {
    "Routes": {
      "proxy-one-route": {
        "ClusterId": "proxy-one-api",
        "Match": {
          "Path": "/proxyone/{*any}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/proxyone" }
        ]
      }
    },
    "Clusters": {
      "proxy-one-api": {
        "Destinations": {
          "destination1": {
            "Address": "http://other.example.com/"
          }
        }
      }
    }
  }
...
}

どのような事ができるのか、またどのように実装すれば良いのかは公式のドキュメントYARPのGitHubのリポジトリsamplesサンプルソースが多数あります。非常に参考になりました。

検証「YARPで実装したWeb APIをAzure ADで保護してOn-Behalf-Ofフローでトークンを再取得する」

なにをしたのか

YARPの変換処理でOn-Behalf-Ofフローができるか確かめました

なぜそのようなことをしたのか

比較的社内アプリのようなものを作ることが多いため、Web APIMicrosoft.Identity.Webを使用し、Azure ADで保護することが多いです。

そのため、YARPで実装したWeb API(以降YARP APIと呼称します)もAzure ADで保護を行いたいと考えました。

また、YARP APIから接続するWeb APIも別のAzure ADアプリケーションで保護されたWeb APIMicrosoft Graphである可能性があるため

YARP APIの中でアクセストークンを適切に付与し直したいと欲求がありました。

AADアプリで保護したWeb APIから別のAADアプリで保護したWeb APIへのアクセスにOn-Behalf-Ofフローを使うことはしょっちゅうあるため、今回の検証を行いました。

実装内容

まずはYARP APIの保護

Microsoft.Identity.Webを使用して、Startup.csで設定するおなじみの方法で保護できました。

On-Behalf-OfフローでITokenAcquisitionを使用するためにEnableTokenAcquisitionToCallDownstreamApiを使用したりするのもおなじみの方法です。

public void ConfigureServices(IServiceCollection services)
{
  services.AddMicrosoftIdentityWebApiAuthentication(Configuration).EnableTokenAcquisitionToCallDownstreamApi().AddInMemoryTokenCaches();
}

参考:damienbod/AspNetCoreYarp

これで、YARP APIにアクセスするさいにアクセストークンがない場合401が返却されるようになりました。

特定のパスのときにOn-Behalf-Ofフローする

公式のサンプルReverseProxy.Auth.Sampleを参考にしました。公式ドキュメントではTransformと呼ばれている処理を実装します。

まずはルーティング時にTransformするよう設定

appsetting.jsonは下記のように設定しました。

{
...
  "ReverseProxy": {
    "Routes": {
      "proxy-one-route": {
        "ClusterId": "proxy-one-api",
        "AuthorizationPolicy": "default",
        "Match": {
          "Path": "/proxy-one-api/{*any}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/proxy-one-api" },
          { "OnBeHalfOfProxy": "xxxxx" }
        ]
      }
    },
    "Clusters": {
      "proxy-one-api": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:44352/api/"
          }
        }
      }
    }
...
}

proxy-one-apiのパスが来たときにproxy-one-apiのパスをリムーブ、今回実装したOnBeHalfOfProxyの変換を実行の後、https://localhost:44352/api/にアクセスされます。

OnBeHalfOfProxyの変換処理において、取得先のスコープ情報がほしいので、マスクしていますがValue値にはスコープ情報が記述されます。

Startup.csでカスタムTransformを使用するよう設定

OnBeHalfOfProxyStartup.csで設定されます。

public void ConfigureServices(IServiceCollection services)
{
...
  var proxyBuilder = services.AddReverseProxy().AddTransformFactory<ProxyAuthFactory>();
  proxyBuilder.LoadFromConfig(Configuration.GetSection("ReverseProxy"));
...
}

AddTransformFactoryで今回実装したオリジナルのTransformの実装であるProxyAuthFactoryを追加しています。

カスタムTransformの実装

ProxyAuthFactoryは下記のような実装です。実装している内容は記載しているコメントのとおりです

我ながらだいぶ雑味が強いので参考程度に…。

public class ProxyAuthFactory : ITransformFactory
{

    public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)
    {
        // appsetting.jsonで設定するTransformの識別子"OnBeHalfOfProxy"
        if (transformValues.TryGetValue("OnBeHalfOfProxy", out var scope))
        {
            if (string.IsNullOrEmpty(scope))
            {
                throw new ArgumentException("A non-empty OnBeHalfOfProxy value is required");
            }
            context.AddRequestTransform(async transformContext => {
                // Transform時に実行される処理

                // Microsoft.Identity.WebのITokenAcquisitionを使用してアクセストークンを取得し直す。
               // 普通に使用する場合はDIを使用するが、タイミング的にコンストラクタで読み込めないためここでインスタンスを取得する
                var tokenAcquisition = transformContext.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
               // いつものOn-Behalf-Ofフローのアクセストークン取得処理
                var res = await tokenAcquisition.GetAccessTokenForUserAsync(new List<string> { scope });
               // Authorizationヘッダにアクセストークンを付与(Transform)
                transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", res);
            });
            return true;
        }
        return false;
    }

    // appsetting.jsonでこのTransformを使用する設定があった場合行われるチェック処理のようだ
    public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("OnBeHalfOfProxy", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
               // アクセストークンの再取得にスコープ情報がほしいので、Valueを必須にした。
                context.Errors.Add(new ArgumentException("A non-empty OnBeHalfOfProxy value is required"));
            }

            return true; // Matched
        }
        return false;
    }
}

これでproxy-one-apiにアクセスした際に、指定したスコープでアクセストークンが再取得されるようになりました。

例えば、{ "OnBeHalfOfProxy": "user.read" }としたときはuser.readスコープでアクセストークンが再取得されるようになるので

「このパスのときはこのスコープでアクセストークン再取得して」といった設定が非常に簡単にできるようになっています。

APIMなんかで同じことをしたときは、XML上にC#スクリプトでゴニョゴニョする感じだったので、それと比べるとこちらのほうが個人的には好みです。

さいごに

というわけで、軽くYARPを触ってみたメモ書きでした。

APIMはお高すぎる&機能がFatすぎる的な理由で、独自に色々実装したりしていましたが、これを使用することで色々きれいに運用できそうな気がします。

今回検証に使用したソースコードは下記に格納されています。On-Behalf-Ofはもちろんですが

GET~DELETEとか、Formデータ送った場合とか、IFormFile受け取れるのかとか、DELETEでbody突っ込んでみたりとか色々実験したりしています。

github.com

Azure.IdentityのTokenCredentialの認証全部試す(その1)

はじめに

この記事は2021年10月時点の記事になります。

参照時期によっては記載している実装内容が動作しない可能性がありますので

その点ご留意いただければと思います。

Azure.IdentityのTokenCredential

Azure Key Vaultのシークレット値などを使用するときに大活躍してくれるAzure.Identityですが

認証のルールなどがいまいちわかりにくい状態になっています。

DefautCredentialを使うとAzure CLIの認証状態から引っ張ってきたりと、実装側の意図しない動作になることもよくあります。

Key Vaultのシークレットを取得する際に使用するSecretClientなどのインスタンス生成時に引数として提供するTokenCredentialのDocsを見ると認証は色々なものを使用できるようです。

どれがどの認証なんだろうということや、DefaultAzureCredentialの内部で行われているフローが原因でドハマりすることも多いので

とりあえず全部試してどのような認証ができるのか確認してみようと思います。

AuthorizationCodeCredential

AADアプリケーションを利用した認証で取得した承認コードを利用するものです。

承認コードの取得はこちらが参考になると思います。

認証は別のビジネスフローで終了してて別途シークレット取得したいときとかに使用する感じと思います。

ただこれはPKCEに対応していない古い認証方式で取得した認証コードでしか使用できないようです(参考)。

なので、使用範囲は限定的、かつあまり使うという選択肢は取らないほうがよいものという印象です。

下記のように使用します。

var cred = new AuthorizationCodeCredential(<TenantId>, <ClientId>, <ClientSecret>, <AuthCode(0.AVY...)>, new AuthorizationCodeCredentialOptions { RedirectUri = new Uri(@<RedirectUri>) });
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

必要なパラメータから分かる通り、内部でアクセストークンを取得するフローが実行されるので

Azure ADアプリケーションでリダイレクトURLの設定や、シークレットの生成、Azure Key Vaultへのアクセス許可が必要です。

また前述の通りv1エンドポイントを使用する必要があるため、プラットフォームはSingle Page ApplicationではなくWebを選択します。

AzureCliCredential

DefaultAzureCredentialの図中にあるAzure CLIを利用した認証です。

f:id:TakasDev:20211031122508p:plain

Azure CLIを使用している人しかいない開発現場であれば積極的に使ってもいいかと思いますが

個人的にはあまり外部ツールに依存する作りにするのは好きじゃないので僕は積極的に使わないと思います。

下記のように使用します。

var cred = new AzureCliCredential();
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

テナントをいくつも持っている人は下記のようにテナントを指定したほうが良いかもしれません。

var cred = new AzureCliCredential(new AzureCliCredentialOptions { TenantId = TenantId });
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

az loginしているユーザーがKey Vaultにアクセスすることになるので、アクセスポリシーにユーザーを追加しといてあげましょう。

AzurePowerShellCredential

DefaultAzureCredentialの図中にあるAzure PowerShellを利用した認証です。

Azure CLIと同じような感じなので感想は割愛します。

下記のように使用します。

var cred = new AzurePowerShellCredential();
// テナントをいくつも持っている人は下記のようにテナントを指定したほうが良いかもしれません。
// var cred = new AzurePowerShellCredential(new AzurePowerShellCredentialOptions { TenantId = TenantId });
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

ChainedTokenCredential

TokenCredentialの配列を食わせることができます。

f:id:TakasDev:20211031122508p:plain

👆のような構成を自前で作り動作させることができます。

与えられた配列の先頭から検証が行われるようです(参考)

下記のように使用します。

var f = new AzurePowerShellCredential(new AzurePowerShellCredentialOptions { TenantId = TenantId });
var s = new AzureCliCredential(new AzureCliCredentialOptions { TenantId = TenantId });
var t = new ClientCertificateCredential(TenantId, ClientId, CertPath);
var list = new List<TokenCredential> { f, s, t };

var cred = new ChainedTokenCredential(list.ToArray());
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

PowerShell -> Azure CLI -> ClientCertificateの順番でトークン取得が検証/実行されます。

個人的に実装するなら、Env->Managed Identity->Intaractiveな順番が良いかと思ってます。

Azure上に展開するときはEnvironment/ManagedIdentityを参照し、開発時はユーザーがログインすることでシークレットにアクセス可能になります。

上述の通り外部ツールに依存する構成が個人的に好きではないというだけの理由です。

最終的にIntractive見るって書いてあるんですけど、なーんかあんまりいい印象がないので自前で途中に変なのが入らないようにしちゃうのが良いと思ってます。

ClientCertificateCredential

クライアント証明書による認証です。Azure ADアプリケーションに証明書を登録することで使用します。

証明書ストアにある証明書でもローカルのpfxファイルを指定する方法でも使用可能です。

この認証では裏で生成されるトークン内にユーザー情報が付与されません。

そのため、Azure Key VaultのアクセスポリシーにはユーザーではなくAzure ADアプリケーションを設定しておく必要があります。

下記のように使用します。

var cred = new ClientCertificateCredential(<TenantId>, <ClientId>, <CertPath>);
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

個人的にはあまり使わないと思います。

ClientSecretCredential

Azure ADアプリケーションのシークレット認証です。

AuthorizationCodeCredentialと同じくシークレットを使用しますが

ClientCertificateCredentialと同じくユーザー情報を伴わない認証となります。

下記のように使用します。

var cred = new ClientSecretCredential(<TenantId>, <ClientId>, <ClientSecret>);
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

EnvironmentやManaged IdentityがあるためAzureのリソース上で利用する場合はあまり使うことはないと思います。

Azureのリソース外で展開する場合で、ユーザーの認証を伴わない場合に利用することになるかと思います。

DefaultAzureCredential

f:id:TakasDev:20211031122508p:plain

これです。

サンプルなどで頻繁に登場しますし割愛します。

DeviceCodeCredential

Intuneなどで管理されているデバイスで使用できるDeviceCodeフローです。

僕の個人環境はIntune管理されているデバイスはないので割愛します。

EnvironmentCredential

環境変数に設定された値を参照して認証が行われます。

引数として値を指定するClientSecretCredentialUsernamePasswordCredentialと違うのは環境変数内の値を参照する点ですね。

どのような値を設定するかはこちらを参照してください。

下記のように使用します。

var cred = new EnvironmentCredential();
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

Azureのリソースの場合は基本Managed Instanceを利用することになると思います。

Azureのリソース外などで使用する場合に使用する感じですね。

内部で行われているのはClientSecretCredentialUsernamePasswordCredentialと同じなので、設定としてもそれらと同じことをしてあげればOKです。

InteractiveBrowserCredential

名前の通りInteractiveに認証が行われます。ブラウザでユーザーID/パスワード入力画面が立ち上がります。

デスクトップアプリケーションから実行する場合はプラットフォームMobile and Desktop Applicationsで認証が通るようにしておく必要があります。

下記のように使用します(コンソールアプリケーションで使用するための実装なのでその点ご留意ください)。

var cred = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { ClientId = ClientId, TenantId = TenantId, RedirectUri= new Uri(@"http://localhost/") });
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

ローカルからアクセスする場合はこれが一番都合が良いかなと思います。

ManagedIdentityCredential

Azureのリソースに展開する場合はこれがド安定だと思います。

生成されたManaged IdentityをAzure Key Vaultのアクセスポリシーに設定してあげましょう。

下記のように使用します。

var cred = new ManagedIdentityCredential();
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

ただ当然ながらローカルだと使えないので、開発時はChainedTokenCredentialを使って他の認証方法と組み合わせて使うことになると思います。

以下次回

思ったよりボリュームが多かったので残ったものは次回にしようと思います。

ただ、ここまで見てわかったとおり、多くはAzure ADアプリケーションを使用した認証フローで構成されていることがわかります。

認証周りのDocsの内容を抑えておけば、どのような認証ができるのか、どのような実装/設定になるのかといった点が想像しやすくなると思います。

今回実装したもののベースは下記リポジトリに格納しています ※色々自分で検証しやすくしているので、記事中のコードと少し構成が違います

github.com

Azure DevOpsのパイプラインで `version` という名前の変数を作るとNuGetパッケージ生成で面倒くさいことになった話

はじめに

2021年10月時点の情報です。

参照時期によっては動作が変わっている可能性がありますのでご注意ください。

なにをしようとしていたか

.NETのライブラリプロジェクトをビルドしてNuGetパッケージをビルドしようとしています。

ライブラリのバージョンはPrefix/SuffixをCIコマンド上で指定して外からバージョン情報を変更できるようにしたいと考えています。

そこで、Powershellで取得した値をversion変数に格納し、その値をmsbuildタスクのmsbuildArgumentsで使おうとしていました。

しかし、このバージョン指定が全く意図したとおりに動きません。

下記は検証を踏まえて作成した再現可能な必要最低限のymlとcsprojファイルの構成なります。

  • csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    <AssemblyVersion>1.2.3.4</AssemblyVersion>
    <FileVersion>1.2.3.4</FileVersion>
    <VersionPrefix>1.2.3</VersionPrefix>
  </PropertyGroup>
</Project>
  • CIのyml
trigger:
  branches:
    include:
    - master

variables:
  vmImageOfBuild: windows-latest
  version: '1.2.34' #👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈

stages:
  - stage: pre_build
    displayName: PreBuild
    jobs:
      - job: build
        pool:
          vmImage: $(vmImageOfBuild)
        steps:
        - task: NuGetCommand@2
          inputs:
            command: 'restore'
            feedsToUse: 'config'
            includeNuGetOrg: true
            restoreSolution: 'SampleLibrary/*.sln'
        - task: MSBuild@1
          displayName: 'Build PreProd'
          inputs:
            solution: '**/SampleLibrary.csproj'
            configuration: Release

何が起きるか

csprojで指定したバージョンでNuGetパッケージが生成されてほしいところですが

CIのyamlで作成したversion変数の1.2.34でNuGetパッケージがリリースされます。タスクでプロパティなどに指定はしていません。まじか。

f:id:TakasDev:20211017155054p:plain

色々試してみる

VersionPrefixはどうなるのか

csprojの<version></version>でバージョン指定するとそこで指定されたバージョンでNuGetパッケージが発行されます。

ってことは、同じようにVersionPrefixを指定したら変わるのでは?ということで試してみました。

variables:
  vmImageOfBuild: windows-latest
  # version: '1.2.34'
  versionprefix: '3.2.1'

これはcsprojで設定したバージョンで生成してくれました。

f:id:TakasDev:20211017160007p:plain

versionprefix / versionPrefix / VersionPrefix など試してみましたが変わらなかったので、影響があるのはversion変数だけのようです。

msbuildタスクだけなのか

おそらく一番良く使うdotnetタスクでも同様の現象は発生するのでしょうか?

csprojの構成はそのままに、ymlを下記のように変更してみました。

trigger:
  branches:
    include:
    - master

variables:
  vmImageOfBuild: windows-latest
  version: '1.2.34-dev.0'

stages:
  - stage: pre_build
    displayName: PreBuild
    jobs:
      - job: build
        pool:
          vmImage: $(vmImageOfBuild)
        steps:
        - task: DotNetCoreCLI@2
          inputs:
            command: 'restore'
            projects: '**/*.csproj'
        - task: DotNetCoreCLI@2
          inputs:
            command: 'build'
            projects: '**/*.csproj'

結果、パッケージのバージョンは書き換わりました。タスクは関係なく書き換わりそうです。

f:id:TakasDev:20211017161706p:plain

csprojでバージョン指定しなければどうなるだろうか

csprojのXML内からバージョン指定部分をごっそり消して下記のような構成にしてみました。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

</Project>

f:id:TakasDev:20211017162004p:plain

この状態でも書き換わったので、csprojの構成内容に関わらずversionを指定するとライブラリのバージョンが書き換わるようです。

まとめ

事前定義済み変数でもなさそうですし 、そもそもタスクでプロパティ指定していないのにVariable指定で勝手に書き換わるというのもどうにもバグ臭いです。

まぁPrefix/Sufixを外部から指定する方法面倒くさいっちゃ面倒くさいので楽にできるのはありがたいのですが、さすがにこれは…

フィードバックは投げますが、事前定義済み変数など触りの部分しか調べていないので、これが仕様とわかる何かがあれば教えていただきたいです🙇‍♂️

と、いうわけで現状version変数を使用する場合は注意しましょう。というか使わないほうが無難かと思います。

他の変数でも同様の現象が発生する可能性も(めちゃ低いとは思いますが)ありますので、どうしても意図と違う動作から抜け出せないときは変数の名前を変更してみると、なにかヒントが得られるかもしれませんね。

パイプライン結果、検証で使用したソースコードなどは下記から確認可能です。

dev.azure.com