diff --git a/go.mod b/go.mod index c16a2cf60..ffc6e9fb2 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect diff --git a/go.sum b/go.sum index 27603623f..5d82703b2 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= diff --git a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential/confidential.go b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential/confidential.go index 549d68ab9..1b3e599b1 100644 --- a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential/confidential.go +++ b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential/confidential.go @@ -359,7 +359,7 @@ func New(authority, clientID string, cred Credential, options ...Option) (Client // authCodeURLOptions contains options for AuthCodeURL type authCodeURLOptions struct { - claims, loginHint, tenantID, domainHint string + claims, loginHint, tenantID, domainHint, prompt string } // AuthCodeURLOption is implemented by options for AuthCodeURL @@ -369,7 +369,7 @@ type AuthCodeURLOption interface { // AuthCodeURL creates a URL used to acquire an authorization code. Users need to call CreateAuthorizationCodeURLParameters and pass it in. // -// Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID] +// Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID], [WithPrompt] func (cca Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, opts ...AuthCodeURLOption) (string, error) { o := authCodeURLOptions{} if err := options.ApplyOptions(&o, opts); err != nil { @@ -382,6 +382,7 @@ func (cca Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, ap.Claims = o.claims ap.LoginHint = o.loginHint ap.DomainHint = o.domainHint + ap.Prompt = o.prompt return cca.base.AuthCodeURL(ctx, clientID, redirectURI, scopes, ap) } @@ -431,6 +432,29 @@ func WithDomainHint(domain string) interface { } } +// WithPrompt adds prompt query parameter in the auth url. +func WithPrompt(prompt shared.Prompt) interface { + AuthCodeURLOption + options.CallOption +} { + return struct { + AuthCodeURLOption + options.CallOption + }{ + CallOption: options.NewCallOption( + func(a any) error { + switch t := a.(type) { + case *authCodeURLOptions: + t.prompt = prompt.String() + default: + return fmt.Errorf("unexpected options type %T", a) + } + return nil + }, + ), + } +} + // WithClaims sets additional claims to request for the token, such as those required by conditional access policies. // Use this option when Azure AD returned a claims challenge for a prior request. The argument must be decoded. // This option is valid for any token acquisition method. @@ -596,6 +620,11 @@ func (cca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts return AuthResult{}, errors.New("call another AcquireToken method to request a new token having these claims") } + // For service principal scenarios, require WithSilentAccount for public API + if o.account.IsZero() { + return AuthResult{}, errors.New("WithSilentAccount option is required") + } + silentParameters := base.AcquireTokenSilentParameters{ Scopes: scopes, Account: o.account, @@ -604,8 +633,15 @@ func (cca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts IsAppCache: o.account.IsZero(), TenantID: o.tenantID, AuthnScheme: o.authnScheme, + Claims: o.claims, } + return cca.acquireTokenSilentInternal(ctx, silentParameters) +} + +// acquireTokenSilentInternal is the internal implementation shared by AcquireTokenSilent and AcquireTokenByCredential +func (cca Client) acquireTokenSilentInternal(ctx context.Context, silentParameters base.AcquireTokenSilentParameters) (AuthResult, error) { + return cca.base.AcquireTokenSilent(ctx, silentParameters) } @@ -708,8 +744,10 @@ func (cca Client) AcquireTokenByAuthCode(ctx context.Context, code string, redir // acquireTokenByCredentialOptions contains optional configuration for AcquireTokenByCredential type acquireTokenByCredentialOptions struct { - claims, tenantID string - authnScheme AuthenticationScheme + claims, tenantID string + authnScheme AuthenticationScheme + extraBodyParameters map[string]string + cacheKeyComponents map[string]string } // AcquireByCredentialOption is implemented by options for AcquireTokenByCredential @@ -719,7 +757,7 @@ type AcquireByCredentialOption interface { // AcquireTokenByCredential acquires a security token from the authority, using the client credentials grant. // -// Options: [WithClaims], [WithTenantID] +// Options: [WithClaims], [WithTenantID], [WithFMIPath], [WithAttribute] func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string, opts ...AcquireByCredentialOption) (AuthResult, error) { o := acquireTokenByCredentialOptions{} err := options.ApplyOptions(&o, opts) @@ -736,6 +774,29 @@ func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string, if o.authnScheme != nil { authParams.AuthnScheme = o.authnScheme } + authParams.ExtraBodyParameters = o.extraBodyParameters + authParams.CacheKeyComponents = o.cacheKeyComponents + if o.claims == "" { + silentParameters := base.AcquireTokenSilentParameters{ + Scopes: scopes, + Account: Account{}, // empty account for app token + RequestType: accesstokens.ATConfidential, + Credential: cca.cred, + IsAppCache: true, + TenantID: o.tenantID, + AuthnScheme: o.authnScheme, + Claims: o.claims, + ExtraBodyParameters: o.extraBodyParameters, + CacheKeyComponents: o.cacheKeyComponents, + } + + // Use internal method with empty account (service principal scenario) + cache, err := cca.acquireTokenSilentInternal(ctx, silentParameters) + if err == nil { + return cache, nil + } + } + token, err := cca.base.Token.Credential(ctx, authParams, cca.cred) if err != nil { return AuthResult{}, err @@ -781,3 +842,63 @@ func (cca Client) Account(ctx context.Context, accountID string) (Account, error func (cca Client) RemoveAccount(ctx context.Context, account Account) error { return cca.base.RemoveAccount(ctx, account) } + +// WithFMIPath specifies the path to a federated managed identity. +// The path should point to a valid FMI configuration file that contains the necessary +// identity information for authentication. +func WithFMIPath(path string) interface { + AcquireByCredentialOption + options.CallOption +} { + return struct { + AcquireByCredentialOption + options.CallOption + }{ + CallOption: options.NewCallOption( + func(a any) error { + switch t := a.(type) { + case *acquireTokenByCredentialOptions: + if t.extraBodyParameters == nil { + t.extraBodyParameters = make(map[string]string) + } + if t.cacheKeyComponents == nil { + t.cacheKeyComponents = make(map[string]string) + } + t.cacheKeyComponents["fmi_path"] = path + t.extraBodyParameters["fmi_path"] = path + default: + return fmt.Errorf("unexpected options type %T", a) + } + return nil + }, + ), + } +} + +// WithAttribute specifies an identity attribute to include in the token request. +// The attribute is sent as "attributes" in the request body and returned as "xmc_attr" +// in the access token claims. This is sometimes used withFMIPath +func WithAttribute(attrValue string) interface { + AcquireByCredentialOption + options.CallOption +} { + return struct { + AcquireByCredentialOption + options.CallOption + }{ + CallOption: options.NewCallOption( + func(a any) error { + switch t := a.(type) { + case *acquireTokenByCredentialOptions: + if t.extraBodyParameters == nil { + t.extraBodyParameters = make(map[string]string) + } + t.extraBodyParameters["attributes"] = attrValue + default: + return fmt.Errorf("unexpected options type %T", a) + } + return nil + }, + ), + } +} diff --git a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/base.go b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/base.go index 61c1c4cec..9bd96090e 100644 --- a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/base.go +++ b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/base.go @@ -46,16 +46,18 @@ type accountManager interface { // AcquireTokenSilentParameters contains the parameters to acquire a token silently (from cache). type AcquireTokenSilentParameters struct { - Scopes []string - Account shared.Account - RequestType accesstokens.AppType - Credential *accesstokens.Credential - IsAppCache bool - TenantID string - UserAssertion string - AuthorizationType authority.AuthorizeType - Claims string - AuthnScheme authority.AuthenticationScheme + Scopes []string + Account shared.Account + RequestType accesstokens.AppType + Credential *accesstokens.Credential + IsAppCache bool + TenantID string + UserAssertion string + AuthorizationType authority.AuthorizeType + Claims string + AuthnScheme authority.AuthenticationScheme + ExtraBodyParameters map[string]string + CacheKeyComponents map[string]string } // AcquireTokenAuthCodeParameters contains the parameters required to acquire an access token using the auth code flow. @@ -300,13 +302,10 @@ func (b Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, s if authParams.DomainHint != "" { v.Add("domain_hint", authParams.DomainHint) } - // There were left over from an implementation that didn't use any of these. We may - // need to add them later, but as of now aren't needed. - /* - if p.ResponseMode != "" { - urlParams.Add("response_mode", p.ResponseMode) - } - */ + // Use form_post response mode for interactive auth to avoid exposing the auth code in the URL + if authParams.AuthorizationType == authority.ATInteractive { + v.Add("response_mode", "form_post") + } baseURL.RawQuery = v.Encode() return baseURL.String(), nil } @@ -327,7 +326,12 @@ func (b Client) AcquireTokenSilent(ctx context.Context, silent AcquireTokenSilen if silent.AuthnScheme != nil { authParams.AuthnScheme = silent.AuthnScheme } - + if silent.CacheKeyComponents != nil { + authParams.CacheKeyComponents = silent.CacheKeyComponents + } + if silent.ExtraBodyParameters != nil { + authParams.ExtraBodyParameters = silent.ExtraBodyParameters + } m := b.pmanager if authParams.AuthorizationType != authority.ATOnBehalfOf { authParams.AuthorizationType = authority.ATRefreshToken @@ -367,8 +371,19 @@ func (b Client) AcquireTokenSilent(ctx context.Context, silent AcquireTokenSilen // If the token is not same, we don't need to refresh it. // Which means it refreshed. if str, err := m.Read(ctx, authParams); err == nil && str.AccessToken.Secret == ar.AccessToken { - if tr, er := b.Token.Credential(ctx, authParams, silent.Credential); er == nil { - return b.AuthResultFromToken(ctx, authParams, tr) + switch silent.RequestType { + case accesstokens.ATConfidential: + if tr, er := b.Token.Credential(ctx, authParams, silent.Credential); er == nil { + return b.AuthResultFromToken(ctx, authParams, tr) + } + case accesstokens.ATPublic: + token, err := b.Token.Refresh(ctx, silent.RequestType, authParams, silent.Credential, storageTokenResponse.RefreshToken) + if err != nil { + return ar, err + } + return b.AuthResultFromToken(ctx, authParams, token) + case accesstokens.ATUnknown: + return ar, errors.New("silent request type cannot be ATUnknown") } } } @@ -446,6 +461,9 @@ func (b Client) AcquireTokenOnBehalfOf(ctx context.Context, onBehalfOfParams Acq authParams.Claims = onBehalfOfParams.Claims authParams.Scopes = onBehalfOfParams.Scopes authParams.UserAssertion = onBehalfOfParams.UserAssertion + if authParams.ExtraBodyParameters != nil { + authParams.ExtraBodyParameters = silentParameters.ExtraBodyParameters + } token, err := b.Token.OnBehalfOf(ctx, authParams, onBehalfOfParams.Credential) if err == nil { ar, err = b.AuthResultFromToken(ctx, authParams, token) diff --git a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage/items.go b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage/items.go index 7379e2233..b7d1a670b 100644 --- a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage/items.go +++ b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage/items.go @@ -79,6 +79,7 @@ type AccessToken struct { UserAssertionHash string `json:"user_assertion_hash,omitempty"` TokenType string `json:"token_type,omitempty"` AuthnSchemeKeyID string `json:"keyid,omitempty"` + ExtCacheKey string `json:"ext_cache_key,omitempty"` AdditionalFields map[string]interface{} } @@ -105,15 +106,21 @@ func NewAccessToken(homeID, env, realm, clientID string, cachedAt, refreshOn, ex // Key outputs the key that can be used to uniquely look up this entry in a map. func (a AccessToken) Key() string { ks := []string{a.HomeAccountID, a.Environment, a.CredentialType, a.ClientID, a.Realm, a.Scopes} - key := strings.Join( - ks, - shared.CacheKeySeparator, - ) + // add token type to key for new access tokens types. skip for bearer token type to // preserve fwd and back compat between a common cache and msal clients if !strings.EqualFold(a.TokenType, authority.AccessTokenTypeBearer) { - key = strings.Join([]string{key, a.TokenType}, shared.CacheKeySeparator) + ks = append(ks, a.TokenType) } + // add extra body param hash to key if present + if a.ExtCacheKey != "" { + ks[2] = "atext" // if the there is extra cache we add "atext" to the key replacing accesstoken + ks = append(ks, a.ExtCacheKey) + } + key := strings.Join( + ks, + shared.CacheKeySeparator, + ) return strings.ToLower(key) } diff --git a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage/storage.go b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage/storage.go index 84a234967..825d8a0f6 100644 --- a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage/storage.go +++ b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage/storage.go @@ -135,7 +135,8 @@ func (m *Manager) Read(ctx context.Context, authParameters authority.AuthParams) aliases = metadata.Aliases } - accessToken := m.readAccessToken(homeAccountID, aliases, realm, clientID, scopes, tokenType, authnSchemeKeyID) + accessToken := m.readAccessToken(homeAccountID, aliases, realm, clientID, scopes, tokenType, authnSchemeKeyID, authParameters.CacheExtKeyGenerator()) + tr.AccessToken = accessToken if homeAccountID == "" { @@ -203,6 +204,7 @@ func (m *Manager) Write(authParameters authority.AuthParams, tokenResponse acces authnSchemeKeyID, ) + accessToken.ExtCacheKey = authParameters.CacheExtKeyGenerator() // Since we have a valid access token, cache it before moving on. if err := accessToken.Validate(); err == nil { if err := m.writeAccessToken(accessToken); err != nil { @@ -291,26 +293,49 @@ func (m *Manager) aadMetadata(ctx context.Context, authorityInfo authority.Info) return m.aadCache[authorityInfo.Host], nil } -func (m *Manager) readAccessToken(homeID string, envAliases []string, realm, clientID string, scopes []string, tokenType, authnSchemeKeyID string) AccessToken { +func (m *Manager) readAccessToken(homeID string, envAliases []string, realm, clientID string, scopes []string, tokenType, authnSchemeKeyID, extCacheKey string) AccessToken { m.contractMu.RLock() - // TODO: linear search (over a map no less) is slow for a large number (thousands) of tokens. - // this shows up as the dominating node in a profile. for real-world scenarios this likely isn't - // an issue, however if it does become a problem then we know where to look. - for k, at := range m.contract.AccessTokens { + + tokensToSearch := m.contract.AccessTokens + + for k, at := range tokensToSearch { + // TODO: linear search (over a map no less) is slow for a large number (thousands) of tokens. + // this shows up as the dominating node in a profile. for real-world scenarios this likely isn't + // an issue, however if it does become a problem then we know where to look. if at.HomeAccountID == homeID && at.Realm == realm && at.ClientID == clientID { - if (strings.EqualFold(at.TokenType, tokenType) && at.AuthnSchemeKeyID == authnSchemeKeyID) || (at.TokenType == "" && (tokenType == "" || tokenType == "Bearer")) { - if checkAlias(at.Environment, envAliases) && isMatchingScopes(scopes, at.Scopes) { - m.contractMu.RUnlock() - if needsUpgrade(k) { - m.contractMu.Lock() - defer m.contractMu.Unlock() - at = upgrade(m.contract.AccessTokens, k) + // Match token type and authentication scheme + tokenTypeMatch := (strings.EqualFold(at.TokenType, tokenType) && at.AuthnSchemeKeyID == authnSchemeKeyID) || + (at.TokenType == "" && (tokenType == "" || tokenType == "Bearer")) + environmentAndScopesMatch := checkAlias(at.Environment, envAliases) && isMatchingScopes(scopes, at.Scopes) + + if tokenTypeMatch && environmentAndScopesMatch { + // For hashed tokens, check that the key contains the hash + if extCacheKey != "" { + if !strings.Contains(k, extCacheKey) { + continue // Skip this token if the key doesn't contain the hash + } + } else { + // If no extCacheKey is provided, only match tokens that also have no extCacheKey + if at.ExtCacheKey != "" { + continue // Skip tokens that require a hash when no hash is provided } + } + // Handle token upgrade if needed + if needsUpgrade(k) { + m.contractMu.RUnlock() + m.contractMu.Lock() + at = upgrade(tokensToSearch, k) + m.contractMu.Unlock() return at } + + m.contractMu.RUnlock() + return at } } } + + // No token found, unlock and return empty token m.contractMu.RUnlock() return AccessToken{} } diff --git a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/local/server.go b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/local/server.go index c6baf2094..1fb012fea 100644 --- a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/local/server.go +++ b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/local/server.go @@ -24,6 +24,7 @@ var okPage = []byte(`

Authentication complete. You can return to the application. Feel free to close this browser tab.

+

For your security: Do not share the contents of this page, the address bar, or take screenshots.

`) @@ -42,6 +43,20 @@ const failPage = ` ` +const unsupportedResponseModePage = ` + + + + + Authentication Failed + + +

Authentication failed. The response was received via a GET operation, which is not supported.

+

You can return to the application. Feel free to close this browser tab.

+ + +` + // Result is the result from the redirect. type Result struct { // Code is the code sent by the authority server. @@ -138,11 +153,24 @@ func (s *Server) putResult(r Result) { } func (s *Server) handler(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() + // Only accept POST requests (form_post response mode) + // GET requests with query parameters are not supported for security reasons + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + _, _ = w.Write([]byte(unsupportedResponseModePage)) + s.putResult(Result{Err: fmt.Errorf("response was received via a GET operation, which is not supported")}) + return + } + + // For form_post response mode, parameters come in the POST body + if err := r.ParseForm(); err != nil { + s.error(w, http.StatusBadRequest, "failed to parse form data: %v", err) + return + } - headerErr := q.Get("error") + headerErr := r.PostFormValue("error") if headerErr != "" { - desc := html.EscapeString(q.Get("error_description")) + desc := html.EscapeString(r.PostFormValue("error_description")) escapedHeaderErr := html.EscapeString(headerErr) // Note: It is a little weird we handle some errors by not going to the failPage. If they all should, // change this to s.error() and make s.error() write the failPage instead of an error code. @@ -152,7 +180,7 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) { return } - respState := q.Get("state") + respState := r.PostFormValue("state") switch respState { case s.reqState: case "": @@ -163,9 +191,9 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) { return } - code := q.Get("code") + code := r.PostFormValue("code") if code == "" { - s.error(w, http.StatusInternalServerError, "authorization code missing in query string") + s.error(w, http.StatusInternalServerError, "authorization code missing in response") return } diff --git a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens/accesstokens.go b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens/accesstokens.go index d738c7591..481f9e434 100644 --- a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens/accesstokens.go +++ b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens/accesstokens.go @@ -281,6 +281,9 @@ func (c Client) FromClientSecret(ctx context.Context, authParameters authority.A qv.Set(clientID, authParameters.ClientID) addScopeQueryParam(qv, authParameters) + // Add extra body parameters if provided + addExtraBodyParameters(ctx, qv, authParameters) + return c.doTokenResp(ctx, authParameters, qv) } @@ -296,6 +299,9 @@ func (c Client) FromAssertion(ctx context.Context, authParameters authority.Auth qv.Set(clientInfo, clientInfoVal) addScopeQueryParam(qv, authParameters) + // Add extra body parameters if provided + addExtraBodyParameters(ctx, qv, authParameters) + return c.doTokenResp(ctx, authParameters, qv) } @@ -329,6 +335,8 @@ func (c Client) FromUserAssertionClientCertificate(ctx context.Context, authPara qv.Set("requested_token_use", "on_behalf_of") addScopeQueryParam(qv, authParameters) + // Add extra body parameters if provided + addExtraBodyParameters(ctx, qv, authParameters) return c.doTokenResp(ctx, authParameters, qv) } @@ -466,3 +474,12 @@ func addScopeQueryParam(queryParams url.Values, authParameters authority.AuthPar scopes := AppendDefaultScopes(authParameters) queryParams.Set("scope", strings.Join(scopes, " ")) } + +// addExtraBodyParameters evaluates and adds extra body parameters to the request +func addExtraBodyParameters(ctx context.Context, v url.Values, ap authority.AuthParams) { + for key, value := range ap.ExtraBodyParameters { + if value != "" { + v.Set(key, value) + } + } +} diff --git a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority/authority.go b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority/authority.go index 3f4037464..debd465db 100644 --- a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority/authority.go +++ b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority/authority.go @@ -15,6 +15,7 @@ import ( "net/url" "os" "path" + "sort" "strings" "time" @@ -47,6 +48,8 @@ type jsonCaller interface { } // For backward compatibility, accept both old and new China endpoints for a transition period. +// This list is derived from the AAD instance discovery metadata and represents all known trusted hosts +// across different Azure clouds (Public, China, Germany, US Government, etc.) var aadTrustedHostList = map[string]bool{ "login.windows.net": true, // Microsoft Azure Worldwide - Used in validation scenarios where host is not this list "login.partner.microsoftonline.cn": true, // Microsoft Azure China (new) @@ -55,6 +58,9 @@ var aadTrustedHostList = map[string]bool{ "login-us.microsoftonline.com": true, // Microsoft Azure US Government - Legacy "login.microsoftonline.us": true, // Microsoft Azure US Government "login.microsoftonline.com": true, // Microsoft Azure Worldwide + "login.microsoft.com": true, + "sts.windows.net": true, + "login.usgovcloudapi.net": true, } // TrustedHost checks if an AAD host is trusted/valid. @@ -103,36 +109,46 @@ func (r *TenantDiscoveryResponse) Validate() error { // ValidateIssuerMatchesAuthority validates that the issuer in the TenantDiscoveryResponse matches the authority. // This is used to identity security or configuration issues in authorities and the OIDC endpoint func (r *TenantDiscoveryResponse) ValidateIssuerMatchesAuthority(authorityURI string, aliases map[string]bool) error { - if authorityURI == "" { return errors.New("TenantDiscoveryResponse: empty authorityURI provided for validation") } + if r.Issuer == "" { + return errors.New("TenantDiscoveryResponse: empty issuer in response") + } - // Parse the issuer URL issuerURL, err := url.Parse(r.Issuer) if err != nil { return fmt.Errorf("TenantDiscoveryResponse: failed to parse issuer URL: %w", err) } + authorityURL, err := url.Parse(authorityURI) + if err != nil { + return fmt.Errorf("TenantDiscoveryResponse: failed to parse authority URL: %w", err) + } + + // Fast path: exact scheme + host match + if issuerURL.Scheme == authorityURL.Scheme && issuerURL.Host == authorityURL.Host { + return nil + } - // Even if it doesn't match the authority, issuers from known and trusted hosts are valid + // Alias-based acceptance if aliases != nil && aliases[issuerURL.Host] { return nil } - // Parse the authority URL for comparison - authorityURL, err := url.Parse(authorityURI) - if err != nil { - return fmt.Errorf("TenantDiscoveryResponse: failed to parse authority URL: %w", err) + issuerHost := issuerURL.Host + authorityHost := authorityURL.Host + + // Accept if issuer host is trusted + if TrustedHost(issuerHost) { + return nil } - // Check if the scheme and host match (paths can be ignored when validating the issuer) - if issuerURL.Scheme == authorityURL.Scheme && issuerURL.Host == authorityURL.Host { + // Accept if authority is a regional variant ending with "." + if strings.HasSuffix(authorityHost, "."+issuerHost) { return nil } - // If we get here, validation failed - return fmt.Errorf("TenantDiscoveryResponse: issuer from OIDC discovery '%s' does not match authority '%s' or a known pattern", - r.Issuer, authorityURI) + return fmt.Errorf("TenantDiscoveryResponse: issuer '%s' does not match authority '%s' or any trusted/alias rule", r.Issuer, authorityURI) } type InstanceDiscoveryMetadata struct { @@ -256,6 +272,12 @@ type AuthParams struct { DomainHint string // AuthnScheme is an optional scheme for formatting access tokens AuthnScheme AuthenticationScheme + // ExtraBodyParameters are additional parameters to include in token requests. + // The functions are evaluated at request time to get the parameter values. + // These parameters are also included in the cache key. + ExtraBodyParameters map[string]string + // CacheKeyComponents are additional components to include in the cache key. + CacheKeyComponents map[string]string } // NewAuthParams creates an authorization parameters object. @@ -642,8 +664,42 @@ func (a *AuthParams) AssertionHash() string { } func (a *AuthParams) AppKey() string { + baseKey := a.ClientID + "_" if a.AuthorityInfo.Tenant != "" { - return fmt.Sprintf("%s_%s_AppTokenCache", a.ClientID, a.AuthorityInfo.Tenant) + baseKey += a.AuthorityInfo.Tenant + } + + // Include extra body parameters in the cache key + paramHash := a.CacheExtKeyGenerator() + if paramHash != "" { + baseKey = fmt.Sprintf("%s_%s", baseKey, paramHash) + } + + return baseKey + "_AppTokenCache" +} + +// CacheExtKeyGenerator computes a hash of the Cache key components key and values +// to include in the cache key. This ensures tokens acquired with different +// parameters are cached separately. +func (a *AuthParams) CacheExtKeyGenerator() string { + if len(a.CacheKeyComponents) == 0 { + return "" + } + + // Sort keys to ensure consistent hashing + keys := make([]string, 0, len(a.CacheKeyComponents)) + for k := range a.CacheKeyComponents { + keys = append(keys, k) } - return fmt.Sprintf("%s__AppTokenCache", a.ClientID) + sort.Strings(keys) + + // Create a string by concatenating key+value pairs + keyStr := "" + for _, key := range keys { + // Append key followed by its value with no separator + keyStr += key + a.CacheKeyComponents[key] + } + + hash := sha256.Sum256([]byte(keyStr)) + return strings.ToLower(base64.RawURLEncoding.EncodeToString(hash[:])) } diff --git a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared/shared.go b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared/shared.go index d8ab71356..77376d6ff 100644 --- a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared/shared.go +++ b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared/shared.go @@ -70,3 +70,29 @@ func (acc Account) IsZero() bool { // DefaultClient is our default shared HTTP client. var DefaultClient = &http.Client{} + +type Prompt int64 + +const ( + PromptNone Prompt = iota + PromptLogin + PromptSelectAccount + PromptConsent + PromptCreate +) + +func (p Prompt) String() string { + switch p { + case PromptNone: + return "none" + case PromptLogin: + return "login" + case PromptSelectAccount: + return "select_account" + case PromptConsent: + return "consent" + case PromptCreate: + return "create" + } + return "" +} diff --git a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/public/public.go b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/public/public.go index 7beed2617..97c615452 100644 --- a/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/public/public.go +++ b/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/public/public.go @@ -149,7 +149,7 @@ func New(clientID string, options ...Option) (Client, error) { // authCodeURLOptions contains options for AuthCodeURL type authCodeURLOptions struct { - claims, loginHint, tenantID, domainHint string + claims, loginHint, tenantID, domainHint, prompt string } // AuthCodeURLOption is implemented by options for AuthCodeURL @@ -159,7 +159,7 @@ type AuthCodeURLOption interface { // AuthCodeURL creates a URL used to acquire an authorization code. // -// Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID] +// Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID], [WithPrompt] func (pca Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, opts ...AuthCodeURLOption) (string, error) { o := authCodeURLOptions{} if err := options.ApplyOptions(&o, opts); err != nil { @@ -172,6 +172,7 @@ func (pca Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, ap.Claims = o.claims ap.LoginHint = o.loginHint ap.DomainHint = o.domainHint + ap.Prompt = o.prompt return pca.base.AuthCodeURL(ctx, clientID, redirectURI, scopes, ap) } @@ -368,9 +369,9 @@ type AcquireByUsernamePasswordOption interface { acquireByUsernamePasswordOption() } -// AcquireTokenByUsernamePassword acquires a security token from the authority, via Username/Password Authentication. -// NOTE: this flow is NOT recommended. +// Deprecated: This API will be removed in a future release. Use a more secure flow instead. Follow this migration guide: https://aka.ms/msal-ropc-migration // +// AcquireTokenByUsernamePassword acquires a security token from the authority, via Username/Password Authentication. // Options: [WithClaims], [WithTenantID] func (pca Client) AcquireTokenByUsernamePassword(ctx context.Context, scopes []string, username, password string, opts ...AcquireByUsernamePasswordOption) (AuthResult, error) { o := acquireTokenByUsernamePasswordOptions{} @@ -526,9 +527,9 @@ func (pca Client) RemoveAccount(ctx context.Context, account Account) error { // interactiveAuthOptions contains the optional parameters used to acquire an access token for interactive auth code flow. type interactiveAuthOptions struct { - claims, domainHint, loginHint, redirectURI, tenantID string - openURL func(url string) error - authnScheme AuthenticationScheme + claims, domainHint, loginHint, redirectURI, tenantID, prompt string + openURL func(url string) error + authnScheme AuthenticationScheme } // AcquireInteractiveOption is implemented by options for AcquireTokenInteractive @@ -590,6 +591,33 @@ func WithDomainHint(domain string) interface { } } +// WithPrompt adds the IdP prompt query parameter in the auth url. +func WithPrompt(prompt shared.Prompt) interface { + AcquireInteractiveOption + AuthCodeURLOption + options.CallOption +} { + return struct { + AcquireInteractiveOption + AuthCodeURLOption + options.CallOption + }{ + CallOption: options.NewCallOption( + func(a any) error { + switch t := a.(type) { + case *authCodeURLOptions: + t.prompt = prompt.String() + case *interactiveAuthOptions: + t.prompt = prompt.String() + default: + return fmt.Errorf("unexpected options type %T", a) + } + return nil + }, + ), + } +} + // WithRedirectURI sets a port for the local server used in interactive authentication, for // example http://localhost:port. All URI components other than the port are ignored. func WithRedirectURI(redirectURI string) interface { @@ -674,7 +702,11 @@ func (pca Client) AcquireTokenInteractive(ctx context.Context, scopes []string, authParams.LoginHint = o.loginHint authParams.DomainHint = o.domainHint authParams.State = uuid.New().String() - authParams.Prompt = "select_account" + if o.prompt != "" { + authParams.Prompt = o.prompt + } else { + authParams.Prompt = shared.PromptSelectAccount.String() + } if o.authnScheme != nil { authParams.AuthnScheme = o.authnScheme } diff --git a/vendor/modules.txt b/vendor/modules.txt index 3d6c892df..19b90a7d7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -48,7 +48,7 @@ github.com/Azure/azure-sdk-for-go/sdk/internal/uuid ## explicit; go 1.16 github.com/Azure/go-ansiterm github.com/Azure/go-ansiterm/winterm -# github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 +# github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 ## explicit; go 1.18 github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential