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アプリの「アプリロール」から設定を行います。
今回は許可されたメンバーの種類「ユーザーやグループ」でReader
とWriter
のロールを作成してみました。
次に作成したロールにユーザーやグループを割り当てます。数赤枠箇所からエンタープライズアプリケーションの画面に移動します。
「ユーザーまたはグループの追加」からユーザーを追加することができます。
この際に、先ほど作成したロールを選択できるようになっています。
認証を行うと下記のように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のidTokenClaims
にroles: 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へのアクセス権限、管理をシステム部門などが容易にしたいという観点からロール部分の実装は自前で行う選択肢が取られることが多いかと思いますが
そこまで厳しい要件がないのであれば、このアプリロールを利用してアプリ内の権限制御を簡単に実装するのもありかと思います。
今回実験で使用したデモは下記に格納しています。
参考
OpenHack DevOpsに参加しました
最近OpenHackのDevOpsカテゴリに参加しましたのでそのレポートとなります。
OpenHack DevOpsとは
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の課題は実際の業務にありがちな問題に対する解決というものだったので、自分たちの業務に落とし込むことも容易でしたし活かせるものは非常に多かったです。
こういった研修(?)に会社で参加するとなると、代表者が参加して持ち帰って共有して…といった流れになりがちかもしれませんが
会社のチームで参加すると議論も深まってより良いものになると感じますので、ぜひチームで参加することも検討してみてください。
ある程度課題を片付けるとデジタルバッチも貰えます(貰えた。良かった。)
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
に設定してEmailMessage
のAttachmensts
の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ベースの通信していそうです。
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まで様々なものが転がっています。
ひょっとしたらここ覗くだけで解決する可能性のほうが高いかもしれません。
コンテナの作りかたを見る
たとえばdotnetとAzure Functions(Java)を組み合わせたいなーとなった場合があるかもしれません。
その場合はそれぞれの.devcontainer
の中にあるDockerfile
やbase.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 APIはMicrosoft.Identity.Web
を使用し、Azure ADで保護することが多いです。
そのため、YARPで実装したWeb API(以降YARP APIと呼称します)もAzure ADで保護を行いたいと考えました。
また、YARP APIから接続するWeb APIも別のAzure ADアプリケーションで保護されたWeb APIやMicrosoft 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(); }
これで、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を使用するよう設定
OnBeHalfOfProxy
はStartup.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突っ込んでみたりとか色々実験したりしています。
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を利用した認証です。
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
の配列を食わせることができます。
👆のような構成を自前で作り動作させることができます。
与えられた配列の先頭から検証が行われるようです(参考)
下記のように使用します。
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
これです。
サンプルなどで頻繁に登場しますし割愛します。
DeviceCodeCredential
Intuneなどで管理されているデバイスで使用できるDeviceCodeフローです。
僕の個人環境はIntune管理されているデバイスはないので割愛します。
EnvironmentCredential
環境変数に設定された値を参照して認証が行われます。
引数として値を指定するClientSecretCredential
やUsernamePasswordCredential
と違うのは環境変数内の値を参照する点ですね。
どのような値を設定するかはこちらを参照してください。
下記のように使用します。
var cred = new EnvironmentCredential(); var client = new SecretClient(new Uri(<KeyVaultUrl>), cred); var d = client.GetSecret("<SecretName>");
Azureのリソースの場合は基本Managed Instanceを利用することになると思います。
Azureのリソース外などで使用する場合に使用する感じですね。
内部で行われているのはClientSecretCredential
やUsernamePasswordCredential
と同じなので、設定としてもそれらと同じことをしてあげれば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の内容を抑えておけば、どのような認証ができるのか、どのような実装/設定になるのかといった点が想像しやすくなると思います。
今回実装したもののベースは下記リポジトリに格納しています ※色々自分で検証しやすくしているので、記事中のコードと少し構成が違います
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パッケージがリリースされます。タスクでプロパティなどに指定はしていません。まじか。
色々試してみる
VersionPrefixはどうなるのか
csprojの<version></version>
でバージョン指定するとそこで指定されたバージョンでNuGetパッケージが発行されます。
ってことは、同じようにVersionPrefix
を指定したら変わるのでは?ということで試してみました。
variables: vmImageOfBuild: windows-latest # version: '1.2.34' versionprefix: '3.2.1'
これはcsprojで設定したバージョンで生成してくれました。
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'
結果、パッケージのバージョンは書き換わりました。タスクは関係なく書き換わりそうです。
csprojでバージョン指定しなければどうなるだろうか
csprojのXML内からバージョン指定部分をごっそり消して下記のような構成にしてみました。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> </PropertyGroup> </Project>
この状態でも書き換わったので、csprojの構成内容に関わらずversion
を指定するとライブラリのバージョンが書き換わるようです。
まとめ
事前定義済み変数でもなさそうですし 、そもそもタスクでプロパティ指定していないのにVariable指定で勝手に書き換わるというのもどうにもバグ臭いです。
まぁPrefix/Sufixを外部から指定する方法面倒くさいっちゃ面倒くさいので楽にできるのはありがたいのですが、さすがにこれは…
フィードバックは投げますが、事前定義済み変数など触りの部分しか調べていないので、これが仕様とわかる何かがあれば教えていただきたいです🙇♂️
と、いうわけで現状version
変数を使用する場合は注意しましょう。というか使わないほうが無難かと思います。
他の変数でも同様の現象が発生する可能性も(めちゃ低いとは思いますが)ありますので、どうしても意図と違う動作から抜け出せないときは変数の名前を変更してみると、なにかヒントが得られるかもしれませんね。
パイプライン結果、検証で使用したソースコードなどは下記から確認可能です。