#author("2021-01-05T10:17:29+00:00","","") #mynavi(Azureメモ); #setlinebreak(on); #html(){{ <style> .images img { border: 1px solid #333; } .images * { vertical-align: top; } .images .img_margin { margin: 0; display: inline-block; } </style> }} * 概要 [#if3d0093] #html(<div class="pl10">) Azure AD ログインを使用するように構成された App Service にGoからアクセスする方法を記載する。 &color(red){ただし、関数アプリなどからアクセスする場合は、当記事で記載しているユーザ名/パスワード認証よりも、クライアントシークレット等を使用したサービス間呼び出し(※)を構成した方がよいと思われる。}; ※[[サービス間の呼び出し用にデーモン クライアント アプリケーションを構成する>https://docs.microsoft.com/ja-jp/azure/app-service/configure-authentication-provider-aad#configure-a-daemon-client-application-for-service-to-service-calls]] #html(</div>) * 目次 [#v91ebec6] #contents - 関連 -- [[Azureメモ]] - 参考 -- [[Azure AD ログインを使用するように App Service または Azure Functions アプリを構成する>https://docs.microsoft.com/ja-jp/azure/app-service/configure-authentication-provider-aad]] -- [[サービス プリンシパルを使用して Azure Active Directory トークンを取得する>https://docs.microsoft.com/ja-jp/azure/databricks/dev-tools/api/latest/aad/service-prin-aad-token]] -- [[Azure Active Directory 認証ライブラリを使用して Azure Active Directory トークンを取得する>https://docs.microsoft.com/ja-jp/azure/databricks/dev-tools/api/latest/aad/app-aad-token]] -- [[Azure Active Directory トークンを使用して認証する>https://docs.microsoft.com/ja-jp/azure/databricks/dev-tools/api/latest/aad/]] -- [[サービス間の呼び出し用にデーモン クライアント アプリケーションを構成する>https://docs.microsoft.com/ja-jp/azure/app-service/configure-authentication-provider-aad#configure-a-daemon-client-application-for-service-to-service-calls]] -- https://github.com/Azure/azure-sdk-for-go -- https://github.com/Azure-Samples/azure-sdk-for-go-samples -- https://github.com/Azure/go-autorest/blob/master/autorest/azure/example/main.go -- https://godoc.org/github.com/Azure/go-autorest/autorest/azure/auth#UsernamePasswordConfig -- https://godoc.org/github.com/Azure/go-autorest/autorest/adal#NewServicePrincipalTokenFromUsernamePassword -- [[Microsoft ID プラットフォームと OAuth 2.0 認証コード フロー>https://docs.microsoft.com/ja-jp/azure/active-directory/develop/v2-oauth2-auth-code-flow]] * 準備 [#u8b11c93] #html(<div class="pl10">) ** リソース作成 [#p9c8653e] #html(<div class="pl10">) #html(){{ <div id="tabs1"> <ul> <li><a href="#tabs1-0">0_env.sh</a></li> <li><a href="#tabs1-1">1_resources.sh</a></li> <li><a href="#tabs1-2">appservice/app.py</a></li> <li><a href="#tabs1-3">appservice/requirements.txt</a></li> </ul> }} // START tabs1-0 #html(<div id="tabs1-0">) #mycode2(){{ #!/bin/bash # 全てのリソース名に付与する接頭文字 (Storageアカウント名などは世界でユニークな必要があるので他ユーザと被らないような名前を付ける) PREFIX=XXXXXXXXXXX # リージョン region=japaneast # リソースグループ名 resourceGroup=${PREFIX}ResourceGroup # App Service webappName=${PREFIX}SampleApp webappPlan=F1 }} #html(</div>) // END tabs1-0 // START tabs1-1 #html(<div id="tabs1-1">) 1_resources.sh #mycode2(){{ #!/bin/bash source ./0_env.sh # リソースの作成 if [ "$1" == "--create" ]; then # リソースグループの作成 echo az group create az group create --name $resourceGroup --location $region # App Service のデプロイ cd appservice az webapp up --sku $webappPlan -n $webappName -l $region -g ${resourceGroup} cd ../ fi # リソースの削除 if [ "$1" == "--delete" ]; then az group delete --name $resourceGroup -y fi }} #html(</div>) // END tabs1-1 // START tabs1-2 #html(<div id="tabs1-2">) appservice/app.py #mycode2(){{ from flask import Flask import json import datetime app = Flask(__name__) @app.route("/hello", methods=['GET', 'POST']) def hello(): message = f"This is Sample response! ({datetime.datetime.now()})" return json.dumps({'message': message}), 200, {'Content-Type': 'application/json; charset=utf-8'} }} #html(</div>) // END tabs1-2 // START tabs1-3 #html(<div id="tabs1-3">) appservice/requirements.txt #mycode2(){{ Flask }} #html(</div>) // END tabs1-3 #html(</div>) // END tabs1 #html(<script>$(function() { $("#tabs1").tabs(); });</script>) #html(</div>) ** アクセス確認用のユーザ作成 [#v8731813] #html(<div class="pl10">) Azure Active Directory → ユーザ からアクアセス確認用のユーザを作成しておく。 #html(<div class="images">) #ref(create_user.png,nolink); #html(</div>) #html(</div>) ** デプロイしたアプリのAAD認証を有効にする [#ea305a3d] #html(<div class="pl10">) App Service から対象のアプリを選択後、[認証/承認] を選択して以下の通り設定。 #html(<div class="images">) #ref(app_auth_setting1.png,nolink); #ref(app_auth_setting2.png,nolink); #html(</div>) #html(</div>) #html(</div>) * プログラムからAAD認証する為の設定 [#a389290b] #html(<div class="pl10">) Active Directory → アプリの登録 から、対象のアプリを選択後、以下の通り設定する。 #html(<div class="images">) #ref(app_settings1.png,nolink); #html(</div>) APIのアクセス許可を下図の通り追加する。(xxxxxSampleApp は今回登録した App Service のアプリ名) #html(<div class="images">) #ref(app_settings2.png,nolink); #html(</div>) #html(</div>) * いったんブラウザからアプリにアクセスする [#c19dd656] #html(<div class="pl10">) ユーザの同意を得る必要があるので、いったんブラウザからアプリにアクセスして対象ユーザでログイン 及び 同意(承認)しておく。 #html(</div>) * GoでAAD認証するコードの作成 [#h857abb1] #html(<div class="pl10">) test.go #mycode2(){{ package main import ( "bytes" "fmt" "io/ioutil" "net/http" //"net/url" "os" "time" "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/Azure/go-autorest/autorest/adal" //"github.com/Azure/go-autorest/autorest/azure" ) const clientID string = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" const tenantID string = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" const username string = "xxxx@xxxx.onmicrosoft.com" const password string = "xxxxxxxx" const resource string = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" const apiUrl string = "https://xxxxxxxxsampleapp.azurewebsites.net/hello" func getToken() string { // トークンの取得 config := auth.NewUsernamePasswordConfig(username, password, clientID, tenantID) fmt.Printf("ClientID: : %v\n" , config.ClientID) fmt.Printf("Username : %v\n" , config.Username) fmt.Printf("TenantID : %v\n" , config.TenantID) fmt.Printf("AADEndpoint: %v\n" , config.AADEndpoint) fmt.Printf("Resource : %v\n" , config.Resource) config.Resource = resource //oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, tenantID) oauthConfig, err := adal.NewOAuthConfig(config.AADEndpoint, tenantID) if err != nil { fmt.Printf("NewOAuthConfig Error! %v\n", err) } spt, sptErr := adal.NewServicePrincipalTokenFromUsernamePassword(*oauthConfig, clientID, username, password, resource) //spt, sptErr := adal.NewServicePrincipalTokenFromUsernamePassword(*oauthConfig, clientID, username, password, config.Resource) if sptErr != nil { fmt.Printf("sptErr: %v\n", sptErr) return "" } err = spt.Refresh() if err != nil { fmt.Printf("Refresh Error: %v\n", err) return "" } //oAuthToken := spt.OAuthToken() token := spt.Token() //fmt.Printf("oAuthToken: %v\n", oAuthToken) //fmt.Printf("token: %v\n", token) fmt.Printf("token.AccessToken : %v\n", token.AccessToken) //fmt.Printf("token.RefreshToken: %v\n", token.RefreshToken) //fmt.Printf("token.ExpiresIn : %v\n", token.ExpiresIn) //fmt.Printf("token.ExpiresOn : %v\n", token.ExpiresOn) //fmt.Printf("token.NotBefore : %v\n", token.NotBefore) fmt.Printf("token.Resource : %v\n", token.Resource) fmt.Printf("token.Type : %v\n", token.Type) return token.AccessToken } func sendRequest() { token := getToken() if token == "" { fmt.Println("GetToken Error") return } client := &http.Client{} reqJson := "{\"param1\": \"abc\"}" req, _ := http.NewRequest("POST", apiUrl, bytes.NewBuffer([]byte(reqJson))) req.Header.Set("Content-Type" , "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) resp, err := client.Do(req) if err != nil { fmt.Printf("Download Error: %v\n", err) } defer resp.Body.Close() fmt.Printf("Download status : %d\n", resp.StatusCode) fmt.Printf("Download length : %d\n", resp.ContentLength) //for k, v := range resp.Header { // fmt.Printf("header %s = %v\n",k, v) //} body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Printf("Read Error: %T\n", err) } // ダウンロードしたデータをローカルに保存 writeData(fmt.Sprintf("receive_data_%s", time.Now().Format("20060102_030405")), body) // コンソールに表示 fmt.Printf("Download data: %v\n", string(body)) } func writeData(filename string, data []byte) error { file, err := os.Create(filename) if err != nil { return err } defer file.Close() _, err = file.Write(data) if err != nil { return err } return nil } func main() { sendRequest() } }} #html(</div>) * 動作確認 [#eed7f41b] #html(<div class="pl10">) トークンは取得できているが、App Service へのアクセス時にエラーになっている。 #myterm2(){{ go run test.go ClientID: : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Username : xxxxxxxx@xxxxxxxx.onmicrosoft.com TenantID : XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX AADEndpoint: https://login.microsoftonline.com/ Resource : https://management.azure.com/ token.AccessToken : eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI...... token.Resource : spn:b7b8579e-XXXX-XXXX-XXXX-XXXXXXXXXXXX token.Type : Bearer Download status : 401 Download length : -1 Download data: {"code":401,"message":"IDX10214: Audience validation failed. Audiences: '[PII is hidden]'. Did not match: validationParameters.ValidAudience: '[PII is hidden]' or validationParameters.ValidAudiences: '[PII is hidden]'."} }} 以下のサイトを参考に対応を実施。 https://medium.com/tech-journo/microsoft-azure-rest-api-authentication-d1ea75704def ※オーディエンスの検証て何やねん。 追加する aud の値はエラー時に表示されているので、これを[許可されるトークン対象ユーザ]として追加する。 #myterm2(){{ : token.AccessToken : eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI...... token.Resource : spn:b7b8579e-XXXX-XXXX-XXXX-XXXXXXXXXXXX <-- これ token.Type : Bearer : }} #html(<div class="images">) #html(<div class="ib">) 対象の App Service の認証/承認から 認証プロバイダの Active Directory を選択。 #ref(app_settings3.png,nolink); #html(</div><div class="ib pl20">) [許可されるトークン対象ユーザ] にエラー時に表示された token.Resource の値を追加。 #ref(app_settings4.png,nolink); #html(</div></div>) 再実行 #myterm2(){{ go run test.go : Download status : 200 Download length : 68 Download data: {"message": "This is Sample response! (2021-01-05 09:45:26.530852)"} }} #html(</div>)