-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
App service support for MI #537
base: andyohart/managed-identity
Are you sure you want to change the base?
Conversation
apps/logger/logger_118.go
Outdated
@@ -0,0 +1,20 @@ | |||
//go:build go1.18 && !go1.20 | |||
|
|||
package logger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please split the logger in a separate commit? It can even go directly to main.
apps/logger/logger_118.go
Outdated
|
||
// Logger struct for Go versions <= 1.20. | ||
type Logger struct { | ||
logCallback CallbackFunc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was under the impression that Azure SDK will not be using the callback for now so there is no need for it. When they upgrade to 1.20, they will start using slog
@@ -378,7 +424,6 @@ func (c Client) retry(maxRetries int, req *http.Request) (*http.Response, error) | |||
var err error | |||
for attempt := 0; attempt < maxRetries; attempt++ { | |||
tryCtx, tryCancel := context.WithTimeout(req.Context(), time.Second*15) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why 15s? I recommend 60s to use the same default as .NET
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Azure SDK has it at 15 seconds, I will update it to 60
q.Set("api-version", appServiceAPIVersion) | ||
q.Set("resource", resource) | ||
switch t := id.(type) { | ||
case UserAssignedClientID: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are general purpose params, not related to App Service. So instead of logging them here, can we instead have logging statements at the start of the MSI "AcquireToken" request, which display all config values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And similarly, after the HTTP request is done ... log failures and in case of success log things like "got an access token", expires in etc. Don't log the actual access token.
apps/logger/logger_120.go
Outdated
if a.logging == nil { | ||
return | ||
} | ||
var slogLevel slog.Level |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unpleaseant to have to do this on every logging call :(.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved with comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blockers are:
- query parameter names (docs)
- returned token expiration time is always 0
- fake token responses in tests don't resemble actual responses
Expiration time is always 0 because App Service responses include only expires_on
(docs) and MSAL unmarshals only expires_in
, using a custom UnmarshalJSON to convert that duration to an instant. Unit tests pass despite this because their fake responses always include expires_in
@@ -377,8 +394,7 @@ func (c Client) retry(maxRetries int, req *http.Request) (*http.Response, error) | |||
var resp *http.Response | |||
var err error | |||
for attempt := 0; attempt < maxRetries; attempt++ { | |||
tryCtx, tryCancel := context.WithTimeout(req.Context(), time.Second*15) | |||
defer tryCancel() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the problem with deferring this?
case UserAssignedResourceID: | ||
q.Set(miQueryParameterResourceId, string(t)) | ||
case UserAssignedObjectID: | ||
q.Set(miQueryParameterObjectId, string(t)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q.Set(miQueryParameterObjectId, string(t)) | |
q.Set("principal_id", string(t)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see this in .net or other msal's
Is this changed for App service only?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but it appears I'm out of date on this point, as the docs now state the API will accept object_id
as an alias for principal_id
. 🤷 I'd still make this change because Azure SDK uses, and tests, principal_id
, and these docs have been incorrect before (i.e. if you want to keep using object_id
, make sure you test it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested with object_id
and it worked, would you recommend that I also test with principal_id
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should run live tests before merging in any case
case UserAssignedClientID: | ||
q.Set(miQueryParameterClientId, string(t)) | ||
case UserAssignedResourceID: | ||
q.Set(miQueryParameterResourceId, string(t)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q.Set(miQueryParameterResourceId, string(t)) | |
q.Set("mi_res_id", string(t)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this change only for App service ?
AzureAD/microsoft-authentication-library-for-dotnet#4911
This bug was created to change this to "msi_res_id"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. That issue is about supporting ACI, which imitates IMDS and requires msi_res_id, in accordance with IMDS docs. App Service requires mi_res_id. Every platform has its own managed identity implementation, so subtle differences like this are normal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We were hoping to use msi_res_id
everywhere. Seems like AppService supports it. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If they support it, they should document it; we shouldn't depend on undocumented behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will test this for on Azure Function with all the different user assigned ways and update this comment on the findings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some testing,
I Created a App service function as assigned a UserAssigned
managed Identity and a SystemAssigned
managed identity.
Then I tried to access a KeyVault
using the token we acquired.
Created 4 different client one for each type
SystemAssgiened()
-- No Querry parameter value in the request.
UserAssignedClientID("")
--client_id
client id was assigned to this parameter
UserAssignedObjectID("")
--object_id
Object id was assigned to this parameter
UserAssignedResourceID("")
--msi_res_id
Resource id was assigned to this parameter
Outcome -
I was able to get AccessToken
(verified the token on the jwt.ms) from all the types of the managed identities, using that token I was able to get the secret that was stored in the KeyVault
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tend to agree with @chlowell on this one though. The documented parameter for App Service (and for all other sources except IMDS I believe) is mi_res_id
- see https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal%2Chttp#rest-endpoint-reference
It'd be safer to use that and use msi_res_id
only for IMDS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will update with this in mind
Updated the resource Id parameter for every source except IMDS
TokenType: "Bearer", | ||
func getSuccessfulResponse(resource string, doesHaveExpireIn bool) ([]byte, error) { | ||
var response SuccessfulResponse | ||
if doesHaveExpireIn { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expires_on
and expires_in
aren't mutually exclusive. IMDS and Cloud Shell responses have both. You want to test that, so this may be a good time to migrate to platform-specific responses.
Also, as JSON, SuccessfulResponse always includes expires_in
because the JSON tag doesn't specify omitempty
:
https://play.golang.com/p/kK9VdA_l4Wd
|
||
// Try to parse ExpiresIn first, then fallback to ExpiresOn | ||
if duration, err := parseDuration(aux.ExpiresIn); err != nil { | ||
println("122121@@") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👀
return num.Int64() | ||
} | ||
|
||
// Try to parse ExpiresIn first, then fallback to ExpiresOn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be the other way around--prefer expires_on
if the response includes it, otherwise calculate from expires_in
?
ExpiresIn json.Number `json:"expires_in"` | ||
ExpiresOn json.Number `json:"expires_on"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could simplify this method by continuing to use internalTime.DurationTime
to calculate ExpiresOn from expires_in
:
ExpiresIn json.Number `json:"expires_in"` | |
ExpiresOn json.Number `json:"expires_on"` | |
ExpiresOn json.Number `json:"expires_on,omitempty"` | |
ExpiresOnCalculated internalTime.DurationTime `json:"expires_in,omitempty"` |
then after unmarshaling, if aux.ExpiresOn
is nonzero, you can convert it to a time.Time
for the TokenResponse
. If that field is zero, ExpiresOnCalculated.T
is the time.Time
you want
Quality Gate passedIssues Measures |
Added support for App service
Added support for logging in Managed Identity