rebase: bump github.com/Azure/azure-sdk-for-go/sdk/azidentity

Bumps [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/internal/v1.5.1...sdk/azcore/v1.6.0)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azidentity
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot] 2024-06-11 20:22:58 +00:00 committed by mergify[bot]
parent ec8017512f
commit b658290b37
67 changed files with 1276 additions and 355 deletions

10
go.mod
View File

@ -46,15 +46,15 @@ require (
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.1.0
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/ansel1/merry v1.6.2 // indirect
github.com/ansel1/merry/v2 v2.0.1 // indirect
@ -90,7 +90,7 @@ require (
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/cel-go v0.17.8 // indirect
github.com/google/gnostic-models v0.6.8 // indirect

25
go.sum
View File

@ -759,12 +759,12 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
github.com/Azure/azure-sdk-for-go v62.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.1.0 h1:h4Zxgmi9oyZL2l8jeg1iRTqPloHktywWcu0nlJmo1tA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.1.0/go.mod h1:LgLGXawqSreJz135Elog0ywTJDsm0Hz2k+N+6ZK35u8=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
@ -781,8 +781,8 @@ github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcP
github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/IBM/keyprotect-go-client v0.5.1/go.mod h1:5TwDM/4FRJq1ZOlwQL1xFahLWQ3TveR88VmL1u3njyI=
@ -928,8 +928,6 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@ -1065,8 +1063,8 @@ github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
@ -1569,8 +1567,9 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=

View File

@ -1,5 +1,56 @@
# Release History
## 1.11.1 (2024-04-02)
### Bugs Fixed
* Pollers that use the `Location` header won't consider `http.StatusRequestTimeout` a terminal failure.
* `runtime.Poller[T].Result` won't consider non-terminal error responses as terminal.
## 1.11.0 (2024-04-01)
### Features Added
* Added `StatusCodes` to `arm/policy.RegistrationOptions` to allow supporting non-standard HTTP status codes during registration.
* Added field `InsecureAllowCredentialWithHTTP` to `azcore.ClientOptions` and dependent authentication pipeline policies.
* Added type `MultipartContent` to the `streaming` package to support multipart/form payloads with custom Content-Type and file name.
### Bugs Fixed
* `runtime.SetMultipartFormData` won't try to stringify `[]byte` values.
* Pollers that use the `Location` header won't consider `http.StatusTooManyRequests` a terminal failure.
### Other Changes
* Update dependencies.
## 1.10.0 (2024-02-29)
### Features Added
* Added logging event `log.EventResponseError` that will contain the contents of `ResponseError.Error()` whenever an `azcore.ResponseError` is created.
* Added `runtime.NewResponseErrorWithErrorCode` for creating an `azcore.ResponseError` with a caller-supplied error code.
* Added type `MatchConditions` for use in conditional requests.
### Bugs Fixed
* Fixed a potential race condition between `NullValue` and `IsNullValue`.
* `runtime.EncodeQueryParams` will escape semicolons before calling `url.ParseQuery`.
### Other Changes
* Update dependencies.
## 1.9.2 (2024-02-06)
### Bugs Fixed
* `runtime.MarshalAsByteArray` and `runtime.MarshalAsJSON` will preserve the preexisting value of the `Content-Type` header.
### Other Changes
* Update to latest version of `internal`.
## 1.9.1 (2023-12-11)
### Bugs Fixed

View File

@ -20,6 +20,11 @@ type BearerTokenOptions struct {
// policy's credential must support multitenant authentication.
AuxiliaryTenants []string
// InsecureAllowCredentialWithHTTP enables authenticated requests over HTTP.
// By default, authenticated requests to an HTTP endpoint are rejected by the client.
// WARNING: setting this to true will allow sending the authentication key in clear text. Use with caution.
InsecureAllowCredentialWithHTTP bool
// Scopes contains the list of permission scopes required for the token.
Scopes []string
}
@ -44,6 +49,11 @@ type RegistrationOptions struct {
// The default valule is 5 minutes.
// NOTE: Setting this to a small value might cause the policy to prematurely fail.
PollingDuration time.Duration
// StatusCodes contains the slice of custom HTTP status codes to use instead
// of the default http.StatusConflict. This should only be set if a service
// returns a non-standard HTTP status code when unregistered.
StatusCodes []int
}
// ClientOptions contains configuration settings for a client's pipeline.

View File

@ -31,6 +31,7 @@ func NewPipeline(module, version string, cred azcore.TokenCredential, plOpts azr
}
authPolicy := NewBearerTokenPolicy(cred, &armpolicy.BearerTokenOptions{
AuxiliaryTenants: options.AuxiliaryTenants,
InsecureAllowCredentialWithHTTP: options.InsecureAllowCredentialWithHTTP,
Scopes: []string{conf.Audience + "/.default"},
})
perRetry := make([]azpolicy.Policy, len(plOpts.PerRetry), len(plOpts.PerRetry)+1)

View File

@ -64,6 +64,7 @@ func NewBearerTokenPolicy(cred azcore.TokenCredential, opts *armpolicy.BearerTok
p.scopes = make([]string, len(opts.Scopes))
copy(p.scopes, opts.Scopes)
p.btp = azruntime.NewBearerTokenPolicy(cred, opts.Scopes, &azpolicy.BearerTokenOptions{
InsecureAllowCredentialWithHTTP: opts.InsecureAllowCredentialWithHTTP,
AuthorizationHandler: azpolicy.AuthorizationHandler{
OnChallenge: p.onChallenge,
OnRequest: p.onRequest,

View File

@ -8,7 +8,6 @@ package runtime
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
@ -16,6 +15,7 @@ import (
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/resource"
armpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
@ -45,6 +45,9 @@ func setDefaults(r *armpolicy.RegistrationOptions) {
if r.PollingDuration == 0 {
r.PollingDuration = 5 * time.Minute
}
if len(r.StatusCodes) == 0 {
r.StatusCodes = []int{http.StatusConflict}
}
}
// NewRPRegistrationPolicy creates a policy object configured using the specified options.
@ -88,7 +91,7 @@ func (r *rpRegistrationPolicy) Do(req *azpolicy.Request) (*http.Response, error)
// make the original request
resp, err = req.Next()
// getting a 409 is the first indication that the RP might need to be registered, check error response
if err != nil || resp.StatusCode != http.StatusConflict {
if err != nil || !runtime.HasStatusCode(resp, r.options.StatusCodes...) {
return resp, err
}
var reqErr requestError
@ -105,17 +108,12 @@ func (r *rpRegistrationPolicy) Do(req *azpolicy.Request) (*http.Response, error)
// to the caller so its error unmarshalling will kick in
return resp, err
}
// RP needs to be registered. start by getting the subscription ID from the original request
subID, err := getSubscription(req.Raw().URL.Path)
res, err := resource.ParseResourceID(req.Raw().URL.Path)
if err != nil {
return resp, err
}
// now get the RP from the error
rp, err = getProvider(reqErr)
if err != nil {
return resp, err
}
logRegistrationExit := func(v interface{}) {
rp = res.ResourceType.Namespace
logRegistrationExit := func(v any) {
log.Writef(LogRPRegistration, "END registration for %s: %v", rp, v)
}
log.Writef(LogRPRegistration, "BEGIN registration for %s", rp)
@ -124,7 +122,7 @@ func (r *rpRegistrationPolicy) Do(req *azpolicy.Request) (*http.Response, error)
rpOps := &providersOperations{
p: r.pipeline,
u: r.endpoint,
subID: subID,
subID: res.SubscriptionID,
}
if _, err = rpOps.Register(&shared.ContextWithDeniedValues{Context: req.Raw().Context()}, rp); err != nil {
logRegistrationExit(err)
@ -189,23 +187,6 @@ func isUnregisteredRPCode(errorCode string) bool {
return false
}
func getSubscription(path string) (string, error) {
parts := strings.Split(path, "/")
for i, v := range parts {
if v == "subscriptions" && (i+1) < len(parts) {
return parts[i+1], nil
}
}
return "", fmt.Errorf("failed to obtain subscription ID from %s", path)
}
func getProvider(re requestError) (string, error) {
if len(re.ServiceError.Details) > 0 {
return re.ServiceError.Details[0].Target, nil
}
return "", errors.New("unexpected empty Details")
}
// minimal error definitions to simplify detection
type requestError struct {
ServiceError *serviceError `json:"error"`
@ -213,12 +194,6 @@ type requestError struct {
type serviceError struct {
Code string `json:"code"`
Details []serviceErrorDetails `json:"details"`
}
type serviceErrorDetails struct {
Code string `json:"code"`
Target string `json:"target"`
}
///////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -23,7 +23,7 @@ pr:
- sdk/azcore/
- eng/
stages:
- template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml
extends:
template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml
parameters:
ServiceDirectory: azcore

View File

@ -8,6 +8,7 @@ package azcore
import (
"reflect"
"sync"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
@ -41,13 +42,28 @@ func NewSASCredential(sas string) *SASCredential {
}
// holds sentinel values used to send nulls
var nullables map[reflect.Type]interface{} = map[reflect.Type]interface{}{}
var nullables map[reflect.Type]any = map[reflect.Type]any{}
var nullablesMu sync.RWMutex
// NullValue is used to send an explicit 'null' within a request.
// This is typically used in JSON-MERGE-PATCH operations to delete a value.
func NullValue[T any]() T {
t := shared.TypeOfT[T]()
nullablesMu.RLock()
v, found := nullables[t]
nullablesMu.RUnlock()
if found {
// return the sentinel object
return v.(T)
}
// promote to exclusive lock and check again (double-checked locking pattern)
nullablesMu.Lock()
defer nullablesMu.Unlock()
v, found = nullables[t]
if !found {
var o reflect.Value
if k := t.Kind(); k == reflect.Map {
@ -72,6 +88,9 @@ func NullValue[T any]() T {
func IsNullValue[T any](v T) bool {
// see if our map has a sentinel object for this *T
t := reflect.TypeOf(v)
nullablesMu.RLock()
defer nullablesMu.RUnlock()
if o, found := nullables[t]; found {
o1 := reflect.ValueOf(o)
v1 := reflect.ValueOf(v)

View File

@ -46,3 +46,12 @@ func (e ETag) WeakEquals(other ETag) bool {
func (e ETag) IsWeak() bool {
return len(e) >= 4 && strings.HasPrefix(string(e), "W/\"") && strings.HasSuffix(string(e), "\"")
}
// MatchConditions specifies HTTP options for conditional requests.
type MatchConditions struct {
// Optionally limit requests to resources that have a matching ETag.
IfMatch *ETag
// Optionally limit requests to resources that do not match the ETag.
IfNoneMatch *ETag
}

View File

@ -51,15 +51,15 @@ type Request struct {
values opValues
}
type opValues map[reflect.Type]interface{}
type opValues map[reflect.Type]any
// Set adds/changes a value
func (ov opValues) set(value interface{}) {
func (ov opValues) set(value any) {
ov[reflect.TypeOf(value)] = value
}
// Get looks for a value set by SetValue first
func (ov opValues) get(value interface{}) bool {
func (ov opValues) get(value any) bool {
v, ok := ov[reflect.ValueOf(value).Elem().Type()]
if ok {
reflect.ValueOf(value).Elem().Set(reflect.ValueOf(v))
@ -108,7 +108,7 @@ func (req *Request) Next() (*http.Response, error) {
}
// SetOperationValue adds/changes a mutable key/value associated with a single operation.
func (req *Request) SetOperationValue(value interface{}) {
func (req *Request) SetOperationValue(value any) {
if req.values == nil {
req.values = opValues{}
}
@ -116,7 +116,7 @@ func (req *Request) SetOperationValue(value interface{}) {
}
// OperationValue looks for a value set by SetOperationValue().
func (req *Request) OperationValue(value interface{}) bool {
func (req *Request) OperationValue(value any) bool {
if req.values == nil {
return false
}
@ -125,46 +125,11 @@ func (req *Request) OperationValue(value interface{}) bool {
// SetBody sets the specified ReadSeekCloser as the HTTP request body, and sets Content-Type and Content-Length
// accordingly. If the ReadSeekCloser is nil or empty, Content-Length won't be set. If contentType is "",
// Content-Type won't be set.
// Content-Type won't be set, and if it was set, will be deleted.
// Use streaming.NopCloser to turn an io.ReadSeeker into an io.ReadSeekCloser.
func (req *Request) SetBody(body io.ReadSeekCloser, contentType string) error {
var err error
var size int64
if body != nil {
size, err = body.Seek(0, io.SeekEnd) // Seek to the end to get the stream's size
if err != nil {
return err
}
}
if size == 0 {
// treat an empty stream the same as a nil one: assign req a nil body
body = nil
// RFC 9110 specifies a client shouldn't set Content-Length on a request containing no content
// (Del is a no-op when the header has no value)
req.req.Header.Del(shared.HeaderContentLength)
} else {
_, err = body.Seek(0, io.SeekStart)
if err != nil {
return err
}
req.req.Header.Set(shared.HeaderContentLength, strconv.FormatInt(size, 10))
req.Raw().GetBody = func() (io.ReadCloser, error) {
_, err := body.Seek(0, io.SeekStart) // Seek back to the beginning of the stream
return body, err
}
}
// keep a copy of the body argument. this is to handle cases
// where req.Body is replaced, e.g. httputil.DumpRequest and friends.
req.body = body
req.req.Body = body
req.req.ContentLength = size
if contentType == "" {
// Del is a no-op when the header has no value
req.req.Header.Del(shared.HeaderContentType)
} else {
req.req.Header.Set(shared.HeaderContentType, contentType)
}
return nil
// clobber the existing Content-Type to preserve behavior
return SetBody(req, body, contentType, true)
}
// RewindBody seeks the request's Body stream back to the beginning so it can be resent when retrying an operation.
@ -211,3 +176,48 @@ type PolicyFunc func(*Request) (*http.Response, error)
func (pf PolicyFunc) Do(req *Request) (*http.Response, error) {
return pf(req)
}
// SetBody sets the specified ReadSeekCloser as the HTTP request body, and sets Content-Type and Content-Length accordingly.
// - req is the request to modify
// - body is the request body; if nil or empty, Content-Length won't be set
// - contentType is the value for the Content-Type header; if empty, Content-Type will be deleted
// - clobberContentType when true, will overwrite the existing value of Content-Type with contentType
func SetBody(req *Request, body io.ReadSeekCloser, contentType string, clobberContentType bool) error {
var err error
var size int64
if body != nil {
size, err = body.Seek(0, io.SeekEnd) // Seek to the end to get the stream's size
if err != nil {
return err
}
}
if size == 0 {
// treat an empty stream the same as a nil one: assign req a nil body
body = nil
// RFC 9110 specifies a client shouldn't set Content-Length on a request containing no content
// (Del is a no-op when the header has no value)
req.req.Header.Del(shared.HeaderContentLength)
} else {
_, err = body.Seek(0, io.SeekStart)
if err != nil {
return err
}
req.req.Header.Set(shared.HeaderContentLength, strconv.FormatInt(size, 10))
req.Raw().GetBody = func() (io.ReadCloser, error) {
_, err := body.Seek(0, io.SeekStart) // Seek back to the beginning of the stream
return body, err
}
}
// keep a copy of the body argument. this is to handle cases
// where req.Body is replaced, e.g. httputil.DumpRequest and friends.
req.body = body
req.req.Body = body
req.req.ContentLength = size
if contentType == "" {
// Del is a no-op when the header has no value
req.req.Header.Del(shared.HeaderContentType)
} else if req.req.Header.Get(shared.HeaderContentType) == "" || clobberContentType {
req.req.Header.Set(shared.HeaderContentType, contentType)
}
return nil
}

View File

@ -13,6 +13,7 @@ import (
"net/http"
"regexp"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/internal/exported"
)
@ -20,36 +21,45 @@ import (
// NewResponseError creates a new *ResponseError from the provided HTTP response.
// Exported as runtime.NewResponseError().
func NewResponseError(resp *http.Response) error {
respErr := &ResponseError{
StatusCode: resp.StatusCode,
RawResponse: resp,
}
// prefer the error code in the response header
if ec := resp.Header.Get(shared.HeaderXMSErrorCode); ec != "" {
respErr.ErrorCode = ec
return respErr
return NewResponseErrorWithErrorCode(resp, ec)
}
// if we didn't get x-ms-error-code, check in the response body
body, err := exported.Payload(resp, nil)
if err != nil {
// since we're not returning the ResponseError in this
// case we also don't want to write it to the log.
return err
}
var errorCode string
if len(body) > 0 {
if code := extractErrorCodeJSON(body); code != "" {
respErr.ErrorCode = code
} else if code := extractErrorCodeXML(body); code != "" {
respErr.ErrorCode = code
if fromJSON := extractErrorCodeJSON(body); fromJSON != "" {
errorCode = fromJSON
} else if fromXML := extractErrorCodeXML(body); fromXML != "" {
errorCode = fromXML
}
}
return NewResponseErrorWithErrorCode(resp, errorCode)
}
// NewResponseErrorWithErrorCode creates an *azcore.ResponseError from the provided HTTP response and errorCode.
// Exported as runtime.NewResponseErrorWithErrorCode().
func NewResponseErrorWithErrorCode(resp *http.Response, errorCode string) error {
respErr := &ResponseError{
ErrorCode: errorCode,
StatusCode: resp.StatusCode,
RawResponse: resp,
}
log.Write(log.EventResponseError, respErr.Error())
return respErr
}
func extractErrorCodeJSON(body []byte) string {
var rawObj map[string]interface{}
var rawObj map[string]any
if err := json.Unmarshal(body, &rawObj); err != nil {
// not a JSON object
return ""
@ -58,7 +68,7 @@ func extractErrorCodeJSON(body []byte) string {
// check if this is a wrapped error, i.e. { "error": { ... } }
// if so then unwrap it
if wrapped, ok := rawObj["error"]; ok {
unwrapped, ok := wrapped.(map[string]interface{})
unwrapped, ok := wrapped.(map[string]any)
if !ok {
return ""
}

View File

@ -17,22 +17,34 @@ type Event = log.Event
const (
EventRequest = azlog.EventRequest
EventResponse = azlog.EventResponse
EventResponseError = azlog.EventResponseError
EventRetryPolicy = azlog.EventRetryPolicy
EventLRO = azlog.EventLRO
)
// Write invokes the underlying listener with the specified event and message.
// If the event shouldn't be logged or there is no listener then Write does nothing.
func Write(cls log.Event, msg string) {
log.Write(cls, msg)
}
func Writef(cls log.Event, format string, a ...interface{}) {
// Writef invokes the underlying listener with the specified event and formatted message.
// If the event shouldn't be logged or there is no listener then Writef does nothing.
func Writef(cls log.Event, format string, a ...any) {
log.Writef(cls, format, a...)
}
// SetListener will set the Logger to write to the specified listener.
func SetListener(lst func(Event, string)) {
log.SetListener(lst)
}
// Should returns true if the specified log event should be written to the log.
// By default all log events will be logged. Call SetEvents() to limit
// the log events for logging.
// If no listener has been set this will return false.
// Calling this method is useful when the message to log is computationally expensive
// and you want to avoid the overhead if its log event is not enabled.
func Should(cls log.Event) bool {
return log.Should(cls)
}

View File

@ -27,7 +27,7 @@ func Applicable(resp *http.Response) bool {
}
// CanResume returns true if the token can rehydrate this poller type.
func CanResume(token map[string]interface{}) bool {
func CanResume(token map[string]any) bool {
_, ok := token["asyncURL"]
return ok
}

View File

@ -29,7 +29,7 @@ func Applicable(resp *http.Response) bool {
}
// CanResume returns true if the token can rehydrate this poller type.
func CanResume(token map[string]interface{}) bool {
func CanResume(token map[string]any) bool {
t, ok := token["type"]
if !ok {
return false

View File

@ -26,7 +26,7 @@ func Applicable(resp *http.Response) bool {
}
// CanResume returns true if the token can rehydrate this poller type.
func CanResume(token map[string]interface{}) bool {
func CanResume(token map[string]any) bool {
_, ok := token["fakeURL"]
return ok
}

View File

@ -28,7 +28,7 @@ func Applicable(resp *http.Response) bool {
}
// CanResume returns true if the token can rehydrate this poller type.
func CanResume(token map[string]interface{}) bool {
func CanResume(token map[string]any) bool {
t, ok := token["type"]
if !ok {
return false
@ -103,6 +103,10 @@ func (p *Poller[T]) Poll(ctx context.Context) (*http.Response, error) {
} else if resp.StatusCode > 199 && resp.StatusCode < 300 {
// any 2xx other than a 202 indicates success
p.CurState = poller.StatusSucceeded
} else if pollers.IsNonTerminalHTTPStatusCode(resp) {
// the request timed out or is being throttled.
// DO NOT include this as a terminal failure. preserve
// the existing state and return the response.
} else {
p.CurState = poller.StatusFailed
}

View File

@ -25,7 +25,7 @@ func Applicable(resp *http.Response) bool {
}
// CanResume returns true if the token can rehydrate this poller type.
func CanResume(token map[string]interface{}) bool {
func CanResume(token map[string]any) bool {
_, ok := token["oplocURL"]
return ok
}

View File

@ -74,7 +74,7 @@ func ExtractToken(token string) ([]byte, error) {
// IsTokenValid returns an error if the specified token isn't applicable for generic type T.
func IsTokenValid[T any](token string) error {
raw := map[string]interface{}{}
raw := map[string]any{}
if err := json.Unmarshal([]byte(token), &raw); err != nil {
return err
}
@ -185,3 +185,16 @@ func ResultHelper[T any](resp *http.Response, failed bool, out *T) error {
}
return nil
}
// IsNonTerminalHTTPStatusCode returns true if the HTTP status code should be
// considered non-terminal thus eligible for retry.
func IsNonTerminalHTTPStatusCode(resp *http.Response) bool {
return exported.HasStatusCode(resp,
http.StatusRequestTimeout, // 408
http.StatusTooManyRequests, // 429
http.StatusInternalServerError, // 500
http.StatusBadGateway, // 502
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout, // 504
)
}

View File

@ -40,5 +40,5 @@ const (
Module = "azcore"
// Version is the semantic version (see http://semver.org) of this module.
Version = "v1.9.1"
Version = "v1.11.1"
)

View File

@ -23,6 +23,11 @@ const (
// This includes information like the HTTP status code, headers, and request URL.
EventResponse Event = "Response"
// EventResponseError entries contain information about HTTP responses that returned
// an *azcore.ResponseError (i.e. responses with a non 2xx HTTP status code).
// This includes the contents of ResponseError.Error().
EventResponseError Event = "ResponseError"
// EventRetryPolicy entries contain information specific to the retry policy in use.
EventRetryPolicy Event = "Retry"

View File

@ -39,6 +39,11 @@ type ClientOptions struct {
// Cloud specifies a cloud for the client. The default is Azure Public Cloud.
Cloud cloud.Configuration
// InsecureAllowCredentialWithHTTP enables authenticated requests over HTTP.
// By default, authenticated requests to an HTTP endpoint are rejected by the client.
// WARNING: setting this to true will allow sending the credential in clear text. Use with caution.
InsecureAllowCredentialWithHTTP bool
// Logging configures the built-in logging policy.
Logging LogOptions
@ -147,6 +152,11 @@ type BearerTokenOptions struct {
// When this field isn't set, the policy follows its default behavior of authorizing every request with a bearer token from
// its given credential.
AuthorizationHandler AuthorizationHandler
// InsecureAllowCredentialWithHTTP enables authenticated requests over HTTP.
// By default, authenticated requests to an HTTP endpoint are rejected by the client.
// WARNING: setting this to true will allow sending the bearer token in clear text. Use with caution.
InsecureAllowCredentialWithHTTP bool
}
// AuthorizationHandler allows SDK developers to insert custom logic that runs when BearerTokenPolicy must authorize a request.

View File

@ -14,6 +14,14 @@ import (
// NewResponseError creates an *azcore.ResponseError from the provided HTTP response.
// Call this when a service request returns a non-successful status code.
// The error code will be extracted from the *http.Response, either from the x-ms-error-code
// header (preferred) or attempted to be parsed from the response body.
func NewResponseError(resp *http.Response) error {
return exported.NewResponseError(resp)
}
// NewResponseErrorWithErrorCode creates an *azcore.ResponseError from the provided HTTP response and errorCode.
// Use this variant when the error code is in a non-standard location.
func NewResponseErrorWithErrorCode(resp *http.Response, errorCode string) error {
return exported.NewResponseErrorWithErrorCode(resp, errorCode)
}

View File

@ -24,6 +24,7 @@ type BearerTokenPolicy struct {
authzHandler policy.AuthorizationHandler
cred exported.TokenCredential
scopes []string
allowHTTP bool
}
type acquiringResourceState struct {
@ -55,6 +56,7 @@ func NewBearerTokenPolicy(cred exported.TokenCredential, scopes []string, opts *
cred: cred,
scopes: scopes,
mainResource: temporal.NewResource(acquire),
allowHTTP: opts.InsecureAllowCredentialWithHTTP,
}
}
@ -80,7 +82,7 @@ func (b *BearerTokenPolicy) Do(req *policy.Request) (*http.Response, error) {
return req.Next()
}
if err := checkHTTPSForAuth(req); err != nil {
if err := checkHTTPSForAuth(req, b.allowHTTP); err != nil {
return nil, err
}
@ -113,8 +115,8 @@ func (b *BearerTokenPolicy) Do(req *policy.Request) (*http.Response, error) {
return res, err
}
func checkHTTPSForAuth(req *policy.Request) error {
if strings.ToLower(req.Raw().URL.Scheme) != "https" {
func checkHTTPSForAuth(req *policy.Request, allowHTTP bool) error {
if strings.ToLower(req.Raw().URL.Scheme) != "https" && !allowHTTP {
return errorinfo.NonRetriableError(errors.New("authenticated requests are not permitted for non TLS protected (https) endpoints"))
}
return nil

View File

@ -15,10 +15,16 @@ type KeyCredentialPolicy struct {
cred *exported.KeyCredential
header string
prefix string
allowHTTP bool
}
// KeyCredentialPolicyOptions contains the optional values configuring [KeyCredentialPolicy].
type KeyCredentialPolicyOptions struct {
// InsecureAllowCredentialWithHTTP enables authenticated requests over HTTP.
// By default, authenticated requests to an HTTP endpoint are rejected by the client.
// WARNING: setting this to true will allow sending the authentication key in clear text. Use with caution.
InsecureAllowCredentialWithHTTP bool
// Prefix is used if the key requires a prefix before it's inserted into the HTTP request.
Prefix string
}
@ -35,6 +41,7 @@ func NewKeyCredentialPolicy(cred *exported.KeyCredential, header string, options
cred: cred,
header: header,
prefix: options.Prefix,
allowHTTP: options.InsecureAllowCredentialWithHTTP,
}
}
@ -44,7 +51,7 @@ func (k *KeyCredentialPolicy) Do(req *policy.Request) (*http.Response, error) {
// this prevents a panic that might be hard to diagnose and allows testing
// against http endpoints that don't require authentication.
if k.cred != nil {
if err := checkHTTPSForAuth(req); err != nil {
if err := checkHTTPSForAuth(req, k.allowHTTP); err != nil {
return nil, err
}
val := exported.KeyCredentialGet(k.cred)

View File

@ -14,11 +14,15 @@ import (
type SASCredentialPolicy struct {
cred *exported.SASCredential
header string
allowHTTP bool
}
// SASCredentialPolicyOptions contains the optional values configuring [SASCredentialPolicy].
type SASCredentialPolicyOptions struct {
// placeholder for future optional values
// InsecureAllowCredentialWithHTTP enables authenticated requests over HTTP.
// By default, authenticated requests to an HTTP endpoint are rejected by the client.
// WARNING: setting this to true will allow sending the authentication key in clear text. Use with caution.
InsecureAllowCredentialWithHTTP bool
}
// NewSASCredentialPolicy creates a new instance of [SASCredentialPolicy].
@ -26,9 +30,13 @@ type SASCredentialPolicyOptions struct {
// - header is the name of the HTTP request header in which the shared access signature is placed
// - options contains optional configuration, pass nil to accept the default values
func NewSASCredentialPolicy(cred *exported.SASCredential, header string, options *SASCredentialPolicyOptions) *SASCredentialPolicy {
if options == nil {
options = &SASCredentialPolicyOptions{}
}
return &SASCredentialPolicy{
cred: cred,
header: header,
allowHTTP: options.InsecureAllowCredentialWithHTTP,
}
}
@ -38,7 +46,7 @@ func (k *SASCredentialPolicy) Do(req *policy.Request) (*http.Response, error) {
// this prevents a panic that might be hard to diagnose and allows testing
// against http endpoints that don't require authentication.
if k.cred != nil {
if err := checkHTTPSForAuth(req); err != nil {
if err := checkHTTPSForAuth(req, k.allowHTTP); err != nil {
return nil, err
}
req.Raw().Header.Add(k.header, exported.SASCredentialGet(k.cred))

View File

@ -154,7 +154,7 @@ func NewPollerFromResumeToken[T any](token string, pl exported.Pipeline, options
if err != nil {
return nil, err
}
var asJSON map[string]interface{}
var asJSON map[string]any
if err := json.Unmarshal(raw, &asJSON); err != nil {
return nil, err
}
@ -240,7 +240,7 @@ func (p *Poller[T]) PollUntilDone(ctx context.Context, options *PollUntilDoneOpt
}
start := time.Now()
logPollUntilDoneExit := func(v interface{}) {
logPollUntilDoneExit := func(v any) {
log.Writef(log.EventLRO, "END PollUntilDone() for %T: %v, total time: %s", p.op, v, time.Since(start))
}
log.Writef(log.EventLRO, "BEGIN PollUntilDone() for %T", p.op)
@ -334,6 +334,11 @@ func (p *Poller[T]) Result(ctx context.Context) (res T, err error) {
err = p.op.Result(ctx, p.result)
var respErr *exported.ResponseError
if errors.As(err, &respErr) {
if pollers.IsNonTerminalHTTPStatusCode(respErr.RawResponse) {
// the request failed in a non-terminal way.
// don't cache the error or mark the Poller as done
return
}
// the LRO failed. record the error
p.err = err
} else if err != nil {

View File

@ -11,9 +11,11 @@ import (
"context"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"mime/multipart"
"net/textproto"
"net/url"
"path"
"strings"
@ -21,6 +23,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
)
// Base64Encoding is usesd to specify which base-64 encoder/decoder to use when
@ -42,12 +45,19 @@ func NewRequest(ctx context.Context, httpMethod string, endpoint string) (*polic
}
// EncodeQueryParams will parse and encode any query parameters in the specified URL.
// Any semicolons will automatically be escaped.
func EncodeQueryParams(u string) (string, error) {
before, after, found := strings.Cut(u, "?")
if !found {
return u, nil
}
qp, err := url.ParseQuery(after)
// starting in Go 1.17, url.ParseQuery will reject semicolons in query params.
// so, we must escape them first. note that this assumes that semicolons aren't
// being used as query param separators which is per the current RFC.
// for more info:
// https://github.com/golang/go/issues/25192
// https://github.com/golang/go/issues/50034
qp, err := url.ParseQuery(strings.ReplaceAll(after, ";", "%3B"))
if err != nil {
return "", err
}
@ -97,20 +107,22 @@ func EncodeByteArray(v []byte, format Base64Encoding) string {
func MarshalAsByteArray(req *policy.Request, v []byte, format Base64Encoding) error {
// send as a JSON string
encode := fmt.Sprintf("\"%s\"", EncodeByteArray(v, format))
return req.SetBody(exported.NopCloser(strings.NewReader(encode)), shared.ContentTypeAppJSON)
// tsp generated code can set Content-Type so we must prefer that
return exported.SetBody(req, exported.NopCloser(strings.NewReader(encode)), shared.ContentTypeAppJSON, false)
}
// MarshalAsJSON calls json.Marshal() to get the JSON encoding of v then calls SetBody.
func MarshalAsJSON(req *policy.Request, v interface{}) error {
func MarshalAsJSON(req *policy.Request, v any) error {
b, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("error marshalling type %T: %s", v, err)
}
return req.SetBody(exported.NopCloser(bytes.NewReader(b)), shared.ContentTypeAppJSON)
// tsp generated code can set Content-Type so we must prefer that
return exported.SetBody(req, exported.NopCloser(bytes.NewReader(b)), shared.ContentTypeAppJSON, false)
}
// MarshalAsXML calls xml.Marshal() to get the XML encoding of v then calls SetBody.
func MarshalAsXML(req *policy.Request, v interface{}) error {
func MarshalAsXML(req *policy.Request, v any) error {
b, err := xml.Marshal(v)
if err != nil {
return fmt.Errorf("error marshalling type %T: %s", v, err)
@ -120,10 +132,10 @@ func MarshalAsXML(req *policy.Request, v interface{}) error {
return req.SetBody(exported.NopCloser(bytes.NewReader(b)), shared.ContentTypeAppXML)
}
// SetMultipartFormData writes the specified keys/values as multi-part form
// fields with the specified value. File content must be specified as a ReadSeekCloser.
// All other values are treated as string values.
func SetMultipartFormData(req *policy.Request, formData map[string]interface{}) error {
// SetMultipartFormData writes the specified keys/values as multi-part form fields with the specified value.
// File content must be specified as an [io.ReadSeekCloser] or [streaming.MultipartContent].
// Byte slices will be treated as JSON. All other values are treated as string values.
func SetMultipartFormData(req *policy.Request, formData map[string]any) error {
body := bytes.Buffer{}
writer := multipart.NewWriter(&body)
@ -139,6 +151,60 @@ func SetMultipartFormData(req *policy.Request, formData map[string]interface{})
return nil
}
quoteEscaper := strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
writeMultipartContent := func(fieldname string, mpc streaming.MultipartContent) error {
if mpc.Body == nil {
return errors.New("streaming.MultipartContent.Body cannot be nil")
}
// use fieldname for the file name when unspecified
filename := fieldname
if mpc.ContentType == "" && mpc.Filename == "" {
return writeContent(fieldname, filename, mpc.Body)
}
if mpc.Filename != "" {
filename = mpc.Filename
}
// this is pretty much copied from multipart.Writer.CreateFormFile
// but lets us set the caller provided Content-Type and filename
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
quoteEscaper.Replace(fieldname), quoteEscaper.Replace(filename)))
contentType := "application/octet-stream"
if mpc.ContentType != "" {
contentType = mpc.ContentType
}
h.Set("Content-Type", contentType)
fd, err := writer.CreatePart(h)
if err != nil {
return err
}
// copy the data to the form file
if _, err = io.Copy(fd, mpc.Body); err != nil {
return err
}
return nil
}
// the same as multipart.Writer.WriteField but lets us specify the Content-Type
writeField := func(fieldname, contentType string, value string) error {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"`, quoteEscaper.Replace(fieldname)))
h.Set("Content-Type", contentType)
fd, err := writer.CreatePart(h)
if err != nil {
return err
}
if _, err = fd.Write([]byte(value)); err != nil {
return err
}
return nil
}
for k, v := range formData {
if rsc, ok := v.(io.ReadSeekCloser); ok {
if err := writeContent(k, k, rsc); err != nil {
@ -152,13 +218,35 @@ func SetMultipartFormData(req *policy.Request, formData map[string]interface{})
}
}
continue
} else if mpc, ok := v.(streaming.MultipartContent); ok {
if err := writeMultipartContent(k, mpc); err != nil {
return err
}
continue
} else if mpcs, ok := v.([]streaming.MultipartContent); ok {
for _, mpc := range mpcs {
if err := writeMultipartContent(k, mpc); err != nil {
return err
}
}
continue
}
var content string
contentType := shared.ContentTypeTextPlain
switch tt := v.(type) {
case []byte:
// JSON, don't quote it
content = string(tt)
contentType = shared.ContentTypeAppJSON
case string:
content = tt
default:
// ensure the value is in string format
s, ok := v.(string)
if !ok {
s = fmt.Sprintf("%v", v)
content = fmt.Sprintf("%v", v)
}
if err := writer.WriteField(k, s); err != nil {
if err := writeField(k, contentType, content); err != nil {
return err
}
}

View File

@ -40,7 +40,7 @@ func UnmarshalAsByteArray(resp *http.Response, v *[]byte, format Base64Encoding)
}
// UnmarshalAsJSON calls json.Unmarshal() to unmarshal the received payload into the value pointed to by v.
func UnmarshalAsJSON(resp *http.Response, v interface{}) error {
func UnmarshalAsJSON(resp *http.Response, v any) error {
payload, err := Payload(resp)
if err != nil {
return err
@ -61,7 +61,7 @@ func UnmarshalAsJSON(resp *http.Response, v interface{}) error {
}
// UnmarshalAsXML calls xml.Unmarshal() to unmarshal the received payload into the value pointed to by v.
func UnmarshalAsXML(resp *http.Response, v interface{}) error {
func UnmarshalAsXML(resp *http.Response, v any) error {
payload, err := Payload(resp)
if err != nil {
return err

View File

@ -73,3 +73,17 @@ func (p *progress) Seek(offset int64, whence int) (int64, error) {
func (p *progress) Close() error {
return p.rc.Close()
}
// MultipartContent contains streaming content used in multipart/form payloads.
type MultipartContent struct {
// Body contains the required content body.
Body io.ReadSeekCloser
// ContentType optionally specifies the HTTP Content-Type for this Body.
// The default value is application/octet-stream.
ContentType string
// Filename optionally specifies the filename for this Body.
// The default value is the field name for the multipart/form section.
Filename string
}

View File

@ -0,0 +1,4 @@
# live test artifacts
Dockerfile
k8s.yaml
sshkey*

View File

@ -1,5 +1,66 @@
# Release History
## 1.6.0 (2024-06-10)
### Features Added
* `NewOnBehalfOfCredentialWithClientAssertions` creates an on-behalf-of credential
that authenticates with client assertions such as federated credentials
### Breaking Changes
> These changes affect only code written against a beta version such as v1.6.0-beta.4
* Removed `AzurePipelinesCredential` and the persistent token caching API.
They will return in v1.7.0-beta.1
### Bugs Fixed
* Managed identity bug fixes
## 1.6.0-beta.4 (2024-05-14)
### Features Added
* `AzurePipelinesCredential` authenticates an Azure Pipeline service connection with
workload identity federation
## 1.6.0-beta.3 (2024-04-09)
### Breaking Changes
* `DefaultAzureCredential` now sends a probe request with no retries for IMDS managed identity
environments to avoid excessive retry delays when the IMDS endpoint is not available. This
should improve credential chain resolution for local development scenarios.
### Bugs Fixed
* `ManagedIdentityCredential` now specifies resource IDs correctly for Azure Container Instances
## 1.5.2 (2024-04-09)
### Bugs Fixed
* `ManagedIdentityCredential` now specifies resource IDs correctly for Azure Container Instances
### Other Changes
* Restored v1.4.0 error behavior for empty tenant IDs
* Upgraded dependencies
## 1.6.0-beta.2 (2024-02-06)
### Breaking Changes
> These changes affect only code written against a beta version such as v1.6.0-beta.1
* Replaced `ErrAuthenticationRequired` with `AuthenticationRequiredError`, a struct
type that carries the `TokenRequestOptions` passed to the `GetToken` call which
returned the error.
### Bugs Fixed
* Fixed more cases in which credential chains like `DefaultAzureCredential`
should try their next credential after attempting managed identity
authentication in a Docker Desktop container
### Other Changes
* `AzureCLICredential` uses the CLI's `expires_on` value for token expiration
## 1.6.0-beta.1 (2024-01-17)
### Features Added
* Restored persistent token caching API first added in v1.5.0-beta.1
* Added `AzureCLICredentialOptions.Subscription`
## 1.5.1 (2024-01-17)
### Bugs Fixed
@ -126,7 +187,7 @@
### Features Added
* By default, credentials set client capability "CP1" to enable support for
[Continuous Access Evaluation (CAE)](https://docs.microsoft.com/azure/active-directory/develop/app-resilience-continuous-access-evaluation).
[Continuous Access Evaluation (CAE)](https://learn.microsoft.com/entra/identity-platform/app-resilience-continuous-access-evaluation).
This indicates to Microsoft Entra ID that your application can handle CAE claims challenges.
You can disable this behavior by setting the environment variable "AZURE_IDENTITY_DISABLE_CP1" to "true".
* `InteractiveBrowserCredentialOptions.LoginHint` enables pre-populating the login

View File

@ -1,6 +1,6 @@
# Migrating from autorest/adal to azidentity
`azidentity` provides Microsoft Entra ID ([formerly Azure Active Directory](https://learn.microsoft.com/azure/active-directory/fundamentals/new-name)) authentication for the newest Azure SDK modules (`github.com/azure-sdk-for-go/sdk/...`). Older Azure SDK packages (`github.com/azure-sdk-for-go/services/...`) use types from `github.com/go-autorest/autorest/adal` instead.
`azidentity` provides Microsoft Entra ID ([formerly Azure Active Directory](https://learn.microsoft.com/entra/fundamentals/new-name)) authentication for the newest Azure SDK modules (`github.com/azure-sdk-for-go/sdk/...`). Older Azure SDK packages (`github.com/azure-sdk-for-go/services/...`) use types from `github.com/go-autorest/autorest/adal` instead.
This guide shows common authentication code using `autorest/adal` and its equivalent using `azidentity`.
@ -284,7 +284,7 @@ if err == nil {
}
```
Note that `azidentity` credentials use the Microsoft Entra endpoint, which requires OAuth 2 scopes instead of the resource identifiers `autorest/adal` expects. For more information, see [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/develop/permissions-consent-overview).
Note that `azidentity` credentials use the Microsoft Entra endpoint, which requires OAuth 2 scopes instead of the resource identifiers `autorest/adal` expects. For more information, see [Microsoft Entra ID documentation](https://learn.microsoft.com/entra/identity-platform/permissions-consent-overview).
## Use azidentity credentials with older packages

View File

@ -1,9 +1,9 @@
# Azure Identity Client Module for Go
The Azure Identity module provides Microsoft Entra ID ([formerly Azure Active Directory](https://learn.microsoft.com/azure/active-directory/fundamentals/new-name)) token authentication support across the Azure SDK. It includes a set of `TokenCredential` implementations, which can be used with Azure SDK clients supporting token authentication.
The Azure Identity module provides Microsoft Entra ID ([formerly Azure Active Directory](https://learn.microsoft.com/entra/fundamentals/new-name)) token authentication support across the Azure SDK. It includes a set of `TokenCredential` implementations, which can be used with Azure SDK clients supporting token authentication.
[![PkgGoDev](https://pkg.go.dev/badge/github.com/Azure/azure-sdk-for-go/sdk/azidentity)](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity)
| [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/)
| [Microsoft Entra ID documentation](https://learn.microsoft.com/entra/identity/)
| [Source code](https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/azidentity)
# Getting started
@ -30,7 +30,7 @@ When debugging and executing code locally, developers typically use their own ac
#### Authenticating via the Azure CLI
`DefaultAzureCredential` and `AzureCLICredential` can authenticate as the user
signed in to the [Azure CLI](https://docs.microsoft.com/cli/azure). To sign in to the Azure CLI, run `az login`. On a system with a default web browser, the Azure CLI will launch the browser to authenticate a user.
signed in to the [Azure CLI](https://learn.microsoft.com/cli/azure). To sign in to the Azure CLI, run `az login`. On a system with a default web browser, the Azure CLI will launch the browser to authenticate a user.
When no default browser is available, `az login` will use the device code
authentication flow. This can also be selected manually by running `az login --use-device-code`.
@ -69,14 +69,14 @@ The `azidentity` module focuses on OAuth authentication with Microsoft Entra ID.
## Managed Identity
`DefaultAzureCredential` and `ManagedIdentityCredential` support
[managed identity authentication](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview)
[managed identity authentication](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview)
in any hosting environment which supports managed identities, such as (this list is not exhaustive):
* [Azure App Service](https://docs.microsoft.com/azure/app-service/overview-managed-identity)
* [Azure Arc](https://docs.microsoft.com/azure/azure-arc/servers/managed-identity-authentication)
* [Azure Cloud Shell](https://docs.microsoft.com/azure/cloud-shell/msi-authorization)
* [Azure Kubernetes Service](https://docs.microsoft.com/azure/aks/use-managed-identity)
* [Azure Service Fabric](https://docs.microsoft.com/azure/service-fabric/concepts-managed-identity)
* [Azure Virtual Machines](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token)
* [Azure App Service](https://learn.microsoft.com/azure/app-service/overview-managed-identity)
* [Azure Arc](https://learn.microsoft.com/azure/azure-arc/servers/managed-identity-authentication)
* [Azure Cloud Shell](https://learn.microsoft.com/azure/cloud-shell/msi-authorization)
* [Azure Kubernetes Service](https://learn.microsoft.com/azure/aks/use-managed-identity)
* [Azure Service Fabric](https://learn.microsoft.com/azure/service-fabric/concepts-managed-identity)
* [Azure Virtual Machines](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/how-to-use-vm-token)
## Examples
@ -207,7 +207,7 @@ For more details, see the [token caching documentation](https://aka.ms/azsdk/go/
Credentials return an `error` when they fail to authenticate or lack data they require to authenticate. For guidance on resolving errors from specific credential types, see the [troubleshooting guide](https://aka.ms/azsdk/go/identity/troubleshoot).
For more details on handling specific Microsoft Entra errors, see the Microsoft Entra [error code documentation](https://learn.microsoft.com/azure/active-directory/develop/reference-error-codes).
For more details on handling specific Microsoft Entra errors, see the Microsoft Entra [error code documentation](https://learn.microsoft.com/entra/identity-platform/reference-error-codes).
### Logging

View File

@ -45,7 +45,7 @@ With persistent disk token caching enabled, the library first determines if a va
#### Example code
See the [package documentation](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity@v1.5.0-beta.1#pkg-overview) for code examples demonstrating how to configure persistent caching and access cached data.
See the [package documentation](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity@v1.6.0-beta.2#pkg-overview) for example code demonstrating how to configure persistent caching and access cached data.
### Credentials supporting token caching

View File

@ -58,7 +58,7 @@ This error contains several pieces of information:
- __Failing Credential Type__: The type of credential that failed to authenticate. This can be helpful when diagnosing issues with chained credential types such as `DefaultAzureCredential` or `ChainedTokenCredential`.
- __Microsoft Entra ID Error Code and Message__: The error code and message returned by Microsoft Entra ID. This can give insight into the specific reason the request failed. For instance, in this case authentication failed because the provided client secret is incorrect. [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/develop/reference-error-codes#aadsts-error-codes) has more information on AADSTS error codes.
- __Microsoft Entra ID Error Code and Message__: The error code and message returned by Microsoft Entra ID. This can give insight into the specific reason the request failed. For instance, in this case authentication failed because the provided client secret is incorrect. [Microsoft Entra ID documentation](https://learn.microsoft.com/entra/identity-platform/reference-error-codes#aadsts-error-codes) has more information on AADSTS error codes.
- __Correlation ID and Timestamp__: The correlation ID and timestamp identify the request in server-side logs. This information can be useful to support engineers diagnosing unexpected Microsoft Entra failures.
@ -97,17 +97,17 @@ azlog.SetEvents(azidentity.EventAuthentication)
| Error Code | Issue | Mitigation |
|---|---|---|
|AADSTS7000215|An invalid client secret was provided.|Ensure the secret provided to the credential constructor is valid. If unsure, create a new client secret using the Azure portal. Details on creating a new client secret are in [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret).|
|AADSTS7000222|An expired client secret was provided.|Create a new client secret using the Azure portal. Details on creating a new client secret are in [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret).|
|AADSTS700016|The specified application wasn't found in the specified tenant.|Ensure the client and tenant IDs provided to the credential constructor are correct for your application registration. For multi-tenant apps, ensure the application has been added to the desired tenant by a tenant admin. To add a new application in the desired tenant, follow the [Microsoft Entra ID instructions](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal).|
|AADSTS7000215|An invalid client secret was provided.|Ensure the secret provided to the credential constructor is valid. If unsure, create a new client secret using the Azure portal. Details on creating a new client secret are in [Microsoft Entra ID documentation](https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal#option-2-create-a-new-application-secret).|
|AADSTS7000222|An expired client secret was provided.|Create a new client secret using the Azure portal. Details on creating a new client secret are in [Microsoft Entra ID documentation](https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal#option-2-create-a-new-application-secret).|
|AADSTS700016|The specified application wasn't found in the specified tenant.|Ensure the client and tenant IDs provided to the credential constructor are correct for your application registration. For multi-tenant apps, ensure the application has been added to the desired tenant by a tenant admin. To add a new application in the desired tenant, follow the [Microsoft Entra ID instructions](https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal).|
<a id="client-cert"></a>
## Troubleshoot ClientCertificateCredential authentication issues
| Error Code | Description | Mitigation |
|---|---|---|
|AADSTS700027|Client assertion contains an invalid signature.|Ensure the specified certificate has been uploaded to the application registration as described in [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#option-1-upload-a-certificate).|
|AADSTS700016|The specified application wasn't found in the specified tenant.|Ensure the client and tenant IDs provided to the credential constructor are correct for your application registration. For multi-tenant apps, ensure the application has been added to the desired tenant by a tenant admin. To add a new application in the desired tenant, follow the [Microsoft Entra ID instructions](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal).|
|AADSTS700027|Client assertion contains an invalid signature.|Ensure the specified certificate has been uploaded to the application registration as described in [Microsoft Entra ID documentation](https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal#option-1-upload-a-certificate).|
|AADSTS700016|The specified application wasn't found in the specified tenant.|Ensure the client and tenant IDs provided to the credential constructor are correct for your application registration. For multi-tenant apps, ensure the application has been added to the desired tenant by a tenant admin. To add a new application in the desired tenant, follow the [Microsoft Entra ID instructions](https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal).|
<a id="username-password"></a>
## Troubleshoot UsernamePasswordCredential authentication issues
@ -123,20 +123,20 @@ azlog.SetEvents(azidentity.EventAuthentication)
|Host Environment| | |
|---|---|---|
|Azure Virtual Machines and Scale Sets|[Configuration](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm)|[Troubleshooting](#azure-virtual-machine-managed-identity)|
|Azure App Service and Azure Functions|[Configuration](https://docs.microsoft.com/azure/app-service/overview-managed-identity)|[Troubleshooting](#azure-app-service-and-azure-functions-managed-identity)|
|Azure Virtual Machines and Scale Sets|[Configuration](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vm)|[Troubleshooting](#azure-virtual-machine-managed-identity)|
|Azure App Service and Azure Functions|[Configuration](https://learn.microsoft.com/azure/app-service/overview-managed-identity)|[Troubleshooting](#azure-app-service-and-azure-functions-managed-identity)|
|Azure Kubernetes Service|[Configuration](https://azure.github.io/aad-pod-identity/docs/)|[Troubleshooting](#azure-kubernetes-service-managed-identity)|
|Azure Arc|[Configuration](https://docs.microsoft.com/azure/azure-arc/servers/managed-identity-authentication)||
|Azure Service Fabric|[Configuration](https://docs.microsoft.com/azure/service-fabric/concepts-managed-identity)||
|Azure Arc|[Configuration](https://learn.microsoft.com/azure/azure-arc/servers/managed-identity-authentication)||
|Azure Service Fabric|[Configuration](https://learn.microsoft.com/azure/service-fabric/concepts-managed-identity)||
### Azure Virtual Machine managed identity
| Error Message |Description| Mitigation |
|---|---|---|
|The requested identity hasnt been assigned to this resource.|The IMDS endpoint responded with a status code of 400, indicating the requested identity isnt assigned to the VM.|If using a user assigned identity, ensure the specified ID is correct.<p/><p/>If using a system assigned identity, make sure it has been enabled as described in [managed identity documentation](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm#enable-system-assigned-managed-identity-on-an-existing-vm).|
|The requested identity hasnt been assigned to this resource.|The IMDS endpoint responded with a status code of 400, indicating the requested identity isnt assigned to the VM.|If using a user assigned identity, ensure the specified ID is correct.<p/><p/>If using a system assigned identity, make sure it has been enabled as described in [managed identity documentation](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vm#enable-system-assigned-managed-identity-on-an-existing-vm).|
|The request failed due to a gateway error.|The request to the IMDS endpoint failed due to a gateway error, 502 or 504 status code.|IMDS doesn't support requests via proxy or gateway. Disable proxies or gateways running on the VM for requests to the IMDS endpoint `http://169.254.169.254`|
|No response received from the managed identity endpoint.|No response was received for the request to IMDS or the request timed out.|<ul><li>Ensure the VM is configured for managed identity as described in [managed identity documentation](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm).</li><li>Verify the IMDS endpoint is reachable on the VM. See [below](#verify-imds-is-available-on-the-vm) for instructions.</li></ul>|
|Multiple attempts failed to obtain a token from the managed identity endpoint.|The credential has exhausted its retries for a token request.|<ul><li>Refer to the error message for more details on specific failures.<li>Ensure the VM is configured for managed identity as described in [managed identity documentation](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm).</li><li>Verify the IMDS endpoint is reachable on the VM. See [below](#verify-imds-is-available-on-the-vm) for instructions.</li></ul>|
|No response received from the managed identity endpoint.|No response was received for the request to IMDS or the request timed out.|<ul><li>Ensure the VM is configured for managed identity as described in [managed identity documentation](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vm).</li><li>Verify the IMDS endpoint is reachable on the VM. See [below](#verify-imds-is-available-on-the-vm) for instructions.</li></ul>|
|Multiple attempts failed to obtain a token from the managed identity endpoint.|The credential has exhausted its retries for a token request.|<ul><li>Refer to the error message for more details on specific failures.<li>Ensure the VM is configured for managed identity as described in [managed identity documentation](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vm).</li><li>Verify the IMDS endpoint is reachable on the VM. See [below](#verify-imds-is-available-on-the-vm) for instructions.</li></ul>|
#### Verify IMDS is available on the VM
@ -152,7 +152,7 @@ curl 'http://169.254.169.254/metadata/identity/oauth2/token?resource=https://man
| Error Message |Description| Mitigation |
|---|---|---|
|Get "`http://169.254.169.254/...`" i/o timeout|The App Service host hasn't set environment variables for managed identity configuration.|<ul><li>Ensure the App Service is configured for managed identity as described in [App Service documentation](https://docs.microsoft.com/azure/app-service/overview-managed-identity).</li><li>Verify the App Service environment is properly configured and the managed identity endpoint is available. See [below](#verify-the-app-service-managed-identity-endpoint-is-available) for instructions.</li></ul>|
|Get "`http://169.254.169.254/...`" i/o timeout|The App Service host hasn't set environment variables for managed identity configuration.|<ul><li>Ensure the App Service is configured for managed identity as described in [App Service documentation](https://learn.microsoft.com/azure/app-service/overview-managed-identity).</li><li>Verify the App Service environment is properly configured and the managed identity endpoint is available. See [below](#verify-the-app-service-managed-identity-endpoint-is-available) for instructions.</li></ul>|
#### Verify the App Service managed identity endpoint is available
@ -177,8 +177,8 @@ curl "$IDENTITY_ENDPOINT?resource=https://management.core.windows.net&api-versio
| Error Message |Description| Mitigation |
|---|---|---|
|Azure CLI not found on path|The Azure CLI isnt installed or isn't on the application's path.|<ul><li>Ensure the Azure CLI is installed as described in [Azure CLI documentation](https://docs.microsoft.com/cli/azure/install-azure-cli).</li><li>Validate the installation location is in the application's `PATH` environment variable.</li></ul>|
|Please run 'az login' to set up account|No account is currently logged into the Azure CLI, or the login has expired.|<ul><li>Run `az login` to log into the Azure CLI. More information about Azure CLI authentication is available in the [Azure CLI documentation](https://docs.microsoft.com/cli/azure/authenticate-azure-cli).</li><li>Verify that the Azure CLI can obtain tokens. See [below](#verify-the-azure-cli-can-obtain-tokens) for instructions.</li></ul>|
|Azure CLI not found on path|The Azure CLI isnt installed or isn't on the application's path.|<ul><li>Ensure the Azure CLI is installed as described in [Azure CLI documentation](https://learn.microsoft.com/cli/azure/install-azure-cli).</li><li>Validate the installation location is in the application's `PATH` environment variable.</li></ul>|
|Please run 'az login' to set up account|No account is currently logged into the Azure CLI, or the login has expired.|<ul><li>Run `az login` to log into the Azure CLI. More information about Azure CLI authentication is available in the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli).</li><li>Verify that the Azure CLI can obtain tokens. See [below](#verify-the-azure-cli-can-obtain-tokens) for instructions.</li></ul>|
#### Verify the Azure CLI can obtain tokens

View File

@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "go",
"TagPrefix": "go/azidentity",
"Tag": "go/azidentity_db4a26f583"
"Tag": "go/azidentity_087379b475"
}

View File

@ -121,6 +121,9 @@ func alphanumeric(r rune) bool {
}
func validTenantID(tenantID string) bool {
if len(tenantID) < 1 {
return false
}
for _, r := range tenantID {
if !(alphanumeric(r) || r == '.' || r == '-') {
return false

View File

@ -35,9 +35,9 @@ type AzureCLICredentialOptions struct {
// logged in account can access.
AdditionallyAllowedTenants []string
// subscription is the name or ID of a subscription. Set this to acquire tokens for an account other
// Subscription is the name or ID of a subscription. Set this to acquire tokens for an account other
// than the Azure CLI's current account.
subscription string
Subscription string
// TenantID identifies the tenant the credential should authenticate in.
// Defaults to the CLI's default tenant, which is typically the home tenant of the logged in user.
@ -68,9 +68,9 @@ func NewAzureCLICredential(options *AzureCLICredentialOptions) (*AzureCLICredent
if options != nil {
cp = *options
}
for _, r := range cp.subscription {
for _, r := range cp.Subscription {
if !(alphanumeric(r) || r == '-' || r == '_' || r == ' ' || r == '.') {
return nil, fmt.Errorf("%s: invalid Subscription %q", credNameAzureCLI, cp.subscription)
return nil, fmt.Errorf("%s: invalid Subscription %q", credNameAzureCLI, cp.Subscription)
}
}
if cp.TenantID != "" && !validTenantID(cp.TenantID) {
@ -97,7 +97,7 @@ func (c *AzureCLICredential) GetToken(ctx context.Context, opts policy.TokenRequ
}
c.mu.Lock()
defer c.mu.Unlock()
b, err := c.opts.tokenProvider(ctx, opts.Scopes, tenant, c.opts.subscription)
b, err := c.opts.tokenProvider(ctx, opts.Scopes, tenant, c.opts.Subscription)
if err == nil {
at, err = c.createAccessToken(b)
}
@ -164,25 +164,20 @@ var defaultAzTokenProvider azTokenProvider = func(ctx context.Context, scopes []
func (c *AzureCLICredential) createAccessToken(tk []byte) (azcore.AccessToken, error) {
t := struct {
AccessToken string `json:"accessToken"`
Authority string `json:"_authority"`
ClientID string `json:"_clientId"`
Expires_On int64 `json:"expires_on"`
ExpiresOn string `json:"expiresOn"`
IdentityProvider string `json:"identityProvider"`
IsMRRT bool `json:"isMRRT"`
RefreshToken string `json:"refreshToken"`
Resource string `json:"resource"`
TokenType string `json:"tokenType"`
UserID string `json:"userId"`
}{}
err := json.Unmarshal(tk, &t)
if err != nil {
return azcore.AccessToken{}, err
}
// the Azure CLI's "expiresOn" is local time
exp, err := time.ParseInLocation("2006-01-02 15:04:05.999999", t.ExpiresOn, time.Local)
exp := time.Unix(t.Expires_On, 0)
if t.Expires_On == 0 {
exp, err = time.ParseInLocation("2006-01-02 15:04:05.999999", t.ExpiresOn, time.Local)
if err != nil {
return azcore.AccessToken{}, fmt.Errorf("Error parsing token expiration time %q: %v", t.ExpiresOn, err)
return azcore.AccessToken{}, fmt.Errorf("%s: error parsing token expiration time %q: %v", credNameAzureCLI, t.ExpiresOn, err)
}
}
converted := azcore.AccessToken{

View File

@ -0,0 +1,130 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
)
const (
credNameAzurePipelines = "AzurePipelinesCredential"
oidcAPIVersion = "7.1"
systemAccessToken = "SYSTEM_ACCESSTOKEN"
systemOIDCRequestURI = "SYSTEM_OIDCREQUESTURI"
)
// azurePipelinesCredential authenticates with workload identity federation in an Azure Pipeline. See
// [Azure Pipelines documentation] for more information.
//
// [Azure Pipelines documentation]: https://learn.microsoft.com/azure/devops/pipelines/library/connect-to-azure?view=azure-devops#create-an-azure-resource-manager-service-connection-that-uses-workload-identity-federation
type azurePipelinesCredential struct {
connectionID, oidcURI, systemAccessToken string
cred *ClientAssertionCredential
}
// azurePipelinesCredentialOptions contains optional parameters for AzurePipelinesCredential.
type azurePipelinesCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
}
// newAzurePipelinesCredential is the constructor for AzurePipelinesCredential. In addition to its required arguments,
// it reads a security token for the running build, which is required to authenticate the service connection, from the
// environment variable SYSTEM_ACCESSTOKEN. See the [Azure Pipelines documentation] for an example showing how to set
// this variable in build job YAML.
//
// [Azure Pipelines documentation]: https://learn.microsoft.com/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken
func newAzurePipelinesCredential(tenantID, clientID, serviceConnectionID string, options *azurePipelinesCredentialOptions) (*azurePipelinesCredential, error) {
if options == nil {
options = &azurePipelinesCredentialOptions{}
}
u := os.Getenv(systemOIDCRequestURI)
if u == "" {
return nil, fmt.Errorf("no value for environment variable %s. This should be set by Azure Pipelines", systemOIDCRequestURI)
}
sat := os.Getenv(systemAccessToken)
if sat == "" {
return nil, errors.New("no value for environment variable " + systemAccessToken)
}
a := azurePipelinesCredential{
connectionID: serviceConnectionID,
oidcURI: u,
systemAccessToken: sat,
}
caco := ClientAssertionCredentialOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
cred, err := NewClientAssertionCredential(tenantID, clientID, a.getAssertion, &caco)
if err != nil {
return nil, err
}
cred.client.name = credNameAzurePipelines
a.cred = cred
return &a, nil
}
// GetToken requests an access token from Microsoft Entra ID. Azure SDK clients call this method automatically.
func (a *azurePipelinesCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameAzurePipelines+"."+traceOpGetToken, a.cred.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := a.cred.GetToken(ctx, opts)
return tk, err
}
func (a *azurePipelinesCredential) getAssertion(ctx context.Context) (string, error) {
url := a.oidcURI + "?api-version=" + oidcAPIVersion + "&serviceConnectionId=" + a.connectionID
url, err := runtime.EncodeQueryParams(url)
if err != nil {
return "", newAuthenticationFailedError(credNameAzurePipelines, "couldn't encode OIDC URL: "+err.Error(), nil, nil)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
if err != nil {
return "", newAuthenticationFailedError(credNameAzurePipelines, "couldn't create OIDC token request: "+err.Error(), nil, nil)
}
req.Header.Set("Authorization", "Bearer "+a.systemAccessToken)
res, err := doForClient(a.cred.client.azClient, req)
if err != nil {
return "", newAuthenticationFailedError(credNameAzurePipelines, "couldn't send OIDC token request: "+err.Error(), nil, nil)
}
if res.StatusCode != http.StatusOK {
msg := res.Status + " response from the OIDC endpoint. Check service connection ID and Pipeline configuration"
// include the response because its body, if any, probably contains an error message.
// OK responses aren't included with errors because they probably contain secrets
return "", newAuthenticationFailedError(credNameAzurePipelines, msg, res, nil)
}
b, err := runtime.Payload(res)
if err != nil {
return "", newAuthenticationFailedError(credNameAzurePipelines, "couldn't read OIDC response content: "+err.Error(), nil, nil)
}
var r struct {
OIDCToken string `json:"oidcToken"`
}
err = json.Unmarshal(b, &r)
if err != nil {
return "", newAuthenticationFailedError(credNameAzurePipelines, "unexpected response from OIDC endpoint", nil, nil)
}
return r.OIDCToken, nil
}

View File

@ -86,7 +86,7 @@ func (c *ChainedTokenCredential) GetToken(ctx context.Context, opts policy.Token
errs []error
successfulCredential azcore.TokenCredential
token azcore.AccessToken
unavailableErr *credentialUnavailableError
unavailableErr credentialUnavailable
)
for _, cred := range c.sources {
token, err = cred.GetToken(ctx, opts)

View File

@ -21,15 +21,26 @@ pr:
include:
- sdk/azidentity/
stages:
- template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml
extends:
template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml
parameters:
RunLiveTests: true
UsePipelineProxy: false
ServiceDirectory: 'azidentity'
CloudConfig:
Public:
SubscriptionConfigurations:
- $(sub-config-azure-cloud-test-resources)
# Contains alternate tenant, AAD app and cert info for testing
- $(sub-config-identity-test-resources)
EnvVars:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
RunLiveTests: true
ServiceDirectory: azidentity
UsePipelineProxy: false
${{ if endsWith(variables['Build.DefinitionName'], 'weekly') }}:
MatrixConfigs:
- Name: managed_identity_matrix
GenerateVMJobs: true
Path: sdk/azidentity/managed-identity-matrix.json
Selection: sparse
MatrixReplace:
- Pool=.*LINUXPOOL.*/azsdk-pool-mms-ubuntu-2204-identitymsi
- OSVmImage=.*LINUXNEXTVMIMAGE.*/azsdk-pool-mms-ubuntu-2204-1espt

View File

@ -23,7 +23,7 @@ const credNameAssertion = "ClientAssertionCredential"
// the most common assertion scenario, authenticating a service principal with a certificate. See
// [Microsoft Entra ID documentation] for details of the assertion format.
//
// [Microsoft Entra ID documentation]: https://learn.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#assertion-format
// [Microsoft Entra ID documentation]: https://learn.microsoft.com/entra/identity-platform/certificate-credentials#assertion-format
type ClientAssertionCredential struct {
client *confidentialClient
}

View File

@ -51,7 +51,8 @@ type ClientCertificateCredential struct {
client *confidentialClient
}
// NewClientCertificateCredential constructs a ClientCertificateCredential. Pass nil for options to accept defaults.
// NewClientCertificateCredential constructs a ClientCertificateCredential. Pass nil for options to accept defaults. See
// [ParseCertificates] for help loading a certificate.
func NewClientCertificateCredential(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *ClientCertificateCredentialOptions) (*ClientCertificateCredential, error) {
if len(certs) == 0 {
return nil, errors.New("at least one certificate is required")
@ -86,8 +87,10 @@ func (c *ClientCertificateCredential) GetToken(ctx context.Context, opts policy.
return tk, err
}
// ParseCertificates loads certificates and a private key, in PEM or PKCS12 format, for use with NewClientCertificateCredential.
// Pass nil for password if the private key isn't encrypted. This function can't decrypt keys in PEM format.
// ParseCertificates loads certificates and a private key, in PEM or PKCS#12 format, for use with [NewClientCertificateCredential].
// Pass nil for password if the private key isn't encrypted. This function has limitations, for example it can't decrypt keys in
// PEM format or PKCS#12 certificates that use SHA256 for message authentication. If you encounter such limitations, consider
// using another module to load the certificate and private key.
func ParseCertificates(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) {
var blocks []*pem.Block
var err error

View File

@ -91,7 +91,7 @@ func (c *confidentialClient) GetToken(ctx context.Context, tro policy.TokenReque
}
tro.TenantID = tenant
}
client, mu, err := c.client(ctx, tro)
client, mu, err := c.client(tro)
if err != nil {
return azcore.AccessToken{}, err
}
@ -109,7 +109,7 @@ func (c *confidentialClient) GetToken(ctx context.Context, tro policy.TokenReque
if err != nil {
// We could get a credentialUnavailableError from managed identity authentication because in that case the error comes from our code.
// We return it directly because it affects the behavior of credential chains. Otherwise, we return AuthenticationFailedError.
var unavailableErr *credentialUnavailableError
var unavailableErr credentialUnavailable
if !errors.As(err, &unavailableErr) {
res := getResponseFromError(err)
err = newAuthenticationFailedError(c.name, err.Error(), res, err)
@ -121,7 +121,7 @@ func (c *confidentialClient) GetToken(ctx context.Context, tro policy.TokenReque
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *confidentialClient) client(ctx context.Context, tro policy.TokenRequestOptions) (msalConfidentialClient, *sync.Mutex, error) {
func (c *confidentialClient) client(tro policy.TokenRequestOptions) (msalConfidentialClient, *sync.Mutex, error) {
c.clientMu.Lock()
defer c.clientMu.Unlock()
if tro.EnableCAE {

View File

@ -8,10 +8,8 @@ package azidentity
import (
"context"
"errors"
"os"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
@ -98,13 +96,13 @@ func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*Default
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameWorkloadIdentity, err: err})
}
o := &ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions}
o := &ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions, dac: true}
if ID, ok := os.LookupEnv(azureClientID); ok {
o.ID = ClientID(ID)
}
miCred, err := NewManagedIdentityCredential(o)
if err == nil {
creds = append(creds, &timeoutWrapper{mic: miCred, timeout: time.Second})
creds = append(creds, miCred)
} else {
errorMessages = append(errorMessages, credNameManagedIdentity+": "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameManagedIdentity, err: err})
@ -158,51 +156,10 @@ type defaultCredentialErrorReporter struct {
}
func (d *defaultCredentialErrorReporter) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if _, ok := d.err.(*credentialUnavailableError); ok {
if _, ok := d.err.(credentialUnavailable); ok {
return azcore.AccessToken{}, d.err
}
return azcore.AccessToken{}, newCredentialUnavailableError(d.credType, d.err.Error())
}
var _ azcore.TokenCredential = (*defaultCredentialErrorReporter)(nil)
// timeoutWrapper prevents a potentially very long timeout when managed identity isn't available
type timeoutWrapper struct {
mic *ManagedIdentityCredential
// timeout applies to all auth attempts until one doesn't time out
timeout time.Duration
}
// GetToken wraps DefaultAzureCredential's initial managed identity auth attempt with a short timeout
// because managed identity may not be available and connecting to IMDS can take several minutes to time out.
func (w *timeoutWrapper) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var tk azcore.AccessToken
var err error
// no need to synchronize around this value because it's written only within ChainedTokenCredential's critical section
if w.timeout > 0 {
c, cancel := context.WithTimeout(ctx, w.timeout)
defer cancel()
tk, err = w.mic.GetToken(c, opts)
if isAuthFailedDueToContext(err) {
err = newCredentialUnavailableError(credNameManagedIdentity, "managed identity timed out. See https://aka.ms/azsdk/go/identity/troubleshoot#dac for more information")
} else {
// some managed identity implementation is available, so don't apply the timeout to future calls
w.timeout = 0
}
} else {
tk, err = w.mic.GetToken(ctx, opts)
}
return tk, err
}
// unwraps nested AuthenticationFailedErrors to get the root error
func isAuthFailedDueToContext(err error) bool {
for {
var authFailedErr *AuthenticationFailedError
if !errors.As(err, &authFailedErr) {
break
}
err = authFailedErr.err
}
return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
}

View File

@ -19,7 +19,7 @@ const cliTimeout = 10 * time.Second
// the next credential in its chain (another developer credential).
func unavailableIfInChain(err error, inDefaultChain bool) error {
if err != nil && inDefaultChain {
var unavailableErr *credentialUnavailableError
var unavailableErr credentialUnavailable
if !errors.As(err, &unavailableErr) {
err = newCredentialUnavailableError(credNameAzureDeveloperCLI, err.Error())
}

View File

@ -34,8 +34,8 @@ type DeviceCodeCredentialOptions struct {
ClientID string
// disableAutomaticAuthentication prevents the credential from automatically prompting the user to authenticate.
// When this option is true, [DeviceCodeCredential.GetToken] will return [ErrAuthenticationRequired] when user
// interaction is necessary to acquire a token.
// When this option is true, GetToken will return authenticationRequiredError when user interaction is necessary
// to acquire a token.
disableAutomaticAuthentication bool
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or

View File

@ -57,6 +57,9 @@ type EnvironmentCredentialOptions struct {
//
// AZURE_CLIENT_CERTIFICATE_PASSWORD: (optional) password for the certificate file.
//
// Note that this credential uses [ParseCertificates] to load the certificate and key from the file. If this
// function isn't able to parse your certificate, use [ClientCertificateCredential] instead.
//
// # User with username and password
//
// AZURE_TENANT_ID: (optional) tenant to authenticate in. Defaults to "organizations".
@ -121,7 +124,7 @@ func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*Environme
}
certs, key, err := ParseCertificates(certData, password)
if err != nil {
return nil, fmt.Errorf(`failed to load certificate from "%s": %v`, certPath, err)
return nil, fmt.Errorf("failed to parse %q due to error %q. This may be due to a limitation of this module's certificate loader. Consider calling NewClientCertificateCredential instead", certPath, err.Error())
}
o := &ClientCertificateCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,

View File

@ -13,15 +13,12 @@ import (
"fmt"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo"
msal "github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
)
// errAuthenticationRequired indicates a credential's Authenticate method must be called to acquire a token
// because user interaction is required and the credential is configured not to automatically prompt the user.
var errAuthenticationRequired error = &credentialUnavailableError{"can't acquire a token without user interaction. Call Authenticate to interactively authenticate a user"}
// getResponseFromError retrieves the response carried by
// an AuthenticationFailedError or MSAL CallErr, if any
func getResponseFromError(err error) *http.Response {
@ -56,7 +53,7 @@ func (e *AuthenticationFailedError) Error() string {
return e.credType + ": " + e.message
}
msg := &bytes.Buffer{}
fmt.Fprintf(msg, e.credType+" authentication failed\n")
fmt.Fprintf(msg, "%s authentication failed. %s\n", e.credType, e.message)
if e.RawResponse.Request != nil {
fmt.Fprintf(msg, "%s %s://%s%s\n", e.RawResponse.Request.Method, e.RawResponse.Request.URL.Scheme, e.RawResponse.Request.URL.Host, e.RawResponse.Request.URL.Path)
} else {
@ -110,8 +107,34 @@ func (*AuthenticationFailedError) NonRetriable() {
var _ errorinfo.NonRetriable = (*AuthenticationFailedError)(nil)
// credentialUnavailableError indicates a credential can't attempt authentication because it lacks required
// data or state
// authenticationRequiredError indicates a credential's Authenticate method must be called to acquire a token
// because the credential requires user interaction and is configured not to request it automatically.
type authenticationRequiredError struct {
credentialUnavailableError
// TokenRequestOptions for the required token. Pass this to the credential's Authenticate method.
TokenRequestOptions policy.TokenRequestOptions
}
func newauthenticationRequiredError(credType string, tro policy.TokenRequestOptions) error {
return &authenticationRequiredError{
credentialUnavailableError: credentialUnavailableError{
credType + " can't acquire a token without user interaction. Call Authenticate to authenticate a user interactively",
},
TokenRequestOptions: tro,
}
}
var (
_ credentialUnavailable = (*authenticationRequiredError)(nil)
_ errorinfo.NonRetriable = (*authenticationRequiredError)(nil)
)
type credentialUnavailable interface {
error
credentialUnavailable()
}
type credentialUnavailableError struct {
message string
}
@ -135,6 +158,11 @@ func (e *credentialUnavailableError) Error() string {
}
// NonRetriable is a marker method indicating this error should not be retried. It has no implementation.
func (e *credentialUnavailableError) NonRetriable() {}
func (*credentialUnavailableError) NonRetriable() {}
var _ errorinfo.NonRetriable = (*credentialUnavailableError)(nil)
func (*credentialUnavailableError) credentialUnavailable() {}
var (
_ credentialUnavailable = (*credentialUnavailableError)(nil)
_ errorinfo.NonRetriable = (*credentialUnavailableError)(nil)
)

View File

@ -3,26 +3,40 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0-beta.1/go.mod h1:3Ug6Qzto9an
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -33,7 +47,14 @@ golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -33,8 +33,8 @@ type InteractiveBrowserCredentialOptions struct {
ClientID string
// disableAutomaticAuthentication prevents the credential from automatically prompting the user to authenticate.
// When this option is true, [InteractiveBrowserCredential.GetToken] will return [ErrAuthenticationRequired] when
// user interaction is necessary to acquire a token.
// When this option is true, GetToken will return authenticationRequiredError when user interaction is necessary
// to acquire a token.
disableAutomaticAuthentication bool
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or

View File

@ -0,0 +1,17 @@
{
"include": [
{
"Agent": {
"msi_image": {
"ArmTemplateParameters": "@{deployResources = $true}",
"OSVmImage": "env:LINUXNEXTVMIMAGE",
"Pool": "env:LINUXPOOL"
}
},
"GoVersion": [
"1.22.1"
],
"IDENTITY_IMDS_AVAILABLE": "1"
}
]
}

View File

@ -14,13 +14,15 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
@ -34,16 +36,18 @@ const (
identityServerThumbprint = "IDENTITY_SERVER_THUMBPRINT"
headerMetadata = "Metadata"
imdsEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
miResID = "mi_res_id"
msiEndpoint = "MSI_ENDPOINT"
msiResID = "msi_res_id"
msiSecret = "MSI_SECRET"
imdsAPIVersion = "2018-02-01"
azureArcAPIVersion = "2019-08-15"
serviceFabricAPIVersion = "2019-07-01-preview"
qpClientID = "client_id"
qpResID = "mi_res_id"
serviceFabricAPIVersion = "2019-07-01-preview"
)
var imdsProbeTimeout = time.Second
type msiType int
const (
@ -55,13 +59,28 @@ const (
msiTypeServiceFabric
)
// managedIdentityClient provides the base for authenticating in managed identity environments
// This type includes an runtime.Pipeline and TokenCredentialOptions.
type managedIdentityClient struct {
azClient *azcore.Client
msiType msiType
endpoint string
id ManagedIDKind
msiType msiType
probeIMDS bool
}
// arcKeyDirectory returns the directory expected to contain Azure Arc keys
var arcKeyDirectory = func() (string, error) {
switch runtime.GOOS {
case "linux":
return "/var/opt/azcmagent/tokens", nil
case "windows":
pd := os.Getenv("ProgramData")
if pd == "" {
return "", errors.New("environment variable ProgramData has no value")
}
return filepath.Join(pd, "AzureConnectedMachineAgent", "Tokens"), nil
default:
return "", fmt.Errorf("unsupported OS %q", runtime.GOOS)
}
}
type wrappedNumber json.Number
@ -88,7 +107,7 @@ func setIMDSRetryOptionDefaults(o *policy.RetryOptions) {
if o.StatusCodes == nil {
o.StatusCodes = []int{
// IMDS docs recommend retrying 404, 410, 429 and 5xx
// https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#error-handling
// https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#error-handling
http.StatusNotFound, // 404
http.StatusGone, // 410
http.StatusTooManyRequests, // 429
@ -147,11 +166,12 @@ func newManagedIdentityClient(options *ManagedIdentityCredentialOptions) (*manag
c.msiType = msiTypeCloudShell
}
} else {
c.probeIMDS = options.dac
setIMDSRetryOptionDefaults(&cp.Retry)
}
client, err := azcore.NewClient(module, version, runtime.PipelineOptions{
Tracing: runtime.TracingOptions{
client, err := azcore.NewClient(module, version, azruntime.PipelineOptions{
Tracing: azruntime.TracingOptions{
Namespace: traceNamespace,
},
}, &cp)
@ -180,6 +200,27 @@ func (c *managedIdentityClient) provideToken(ctx context.Context, params confide
// authenticate acquires an access token
func (c *managedIdentityClient) authenticate(ctx context.Context, id ManagedIDKind, scopes []string) (azcore.AccessToken, error) {
// no need to synchronize around this value because it's true only when DefaultAzureCredential constructed the client,
// and in that case ChainedTokenCredential.GetToken synchronizes goroutines that would execute this block
if c.probeIMDS {
cx, cancel := context.WithTimeout(ctx, imdsProbeTimeout)
defer cancel()
cx = policy.WithRetryOptions(cx, policy.RetryOptions{MaxRetries: -1})
req, err := azruntime.NewRequest(cx, http.MethodGet, c.endpoint)
if err == nil {
_, err = c.azClient.Pipeline().Do(req)
}
if err != nil {
msg := err.Error()
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
msg = "managed identity timed out. See https://aka.ms/azsdk/go/identity/troubleshoot#dac for more information"
}
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, msg)
}
// send normal token requests from now on because something responded
c.probeIMDS = false
}
msg, err := c.createAuthRequest(ctx, id, scopes)
if err != nil {
return azcore.AccessToken{}, err
@ -190,7 +231,7 @@ func (c *managedIdentityClient) authenticate(ctx context.Context, id ManagedIDKi
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, err.Error(), nil, err)
}
if runtime.HasStatusCode(resp, http.StatusOK, http.StatusCreated) {
if azruntime.HasStatusCode(resp, http.StatusOK, http.StatusCreated) {
return c.createAccessToken(resp)
}
@ -201,15 +242,15 @@ func (c *managedIdentityClient) authenticate(ctx context.Context, id ManagedIDKi
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "the requested identity isn't assigned to this resource", resp, nil)
}
msg := "failed to authenticate a system assigned identity"
if body, err := runtime.Payload(resp); err == nil && len(body) > 0 {
if body, err := azruntime.Payload(resp); err == nil && len(body) > 0 {
msg += fmt.Sprintf(". The endpoint responded with %s", body)
}
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, msg)
case http.StatusForbidden:
// Docker Desktop runs a proxy that responds 403 to IMDS token requests. If we get that response,
// we return credentialUnavailableError so credential chains continue to their next credential
body, err := runtime.Payload(resp)
if err == nil && strings.Contains(string(body), "A socket operation was attempted to an unreachable network") {
body, err := azruntime.Payload(resp)
if err == nil && strings.Contains(string(body), "unreachable") {
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, fmt.Sprintf("unexpected response %q", string(body)))
}
}
@ -226,7 +267,7 @@ func (c *managedIdentityClient) createAccessToken(res *http.Response) (azcore.Ac
ExpiresIn wrappedNumber `json:"expires_in,omitempty"` // this field should always return the number of seconds for which a token is valid
ExpiresOn interface{} `json:"expires_on,omitempty"` // the value returned in this field varies between a number and a date string
}{}
if err := runtime.UnmarshalAsJSON(res, &value); err != nil {
if err := azruntime.UnmarshalAsJSON(res, &value); err != nil {
return azcore.AccessToken{}, fmt.Errorf("internal AccessToken: %v", err)
}
if value.ExpiresIn != "" {
@ -276,7 +317,7 @@ func (c *managedIdentityClient) createAuthRequest(ctx context.Context, id Manage
}
func (c *managedIdentityClient) createIMDSAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
request, err := azruntime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
@ -286,7 +327,7 @@ func (c *managedIdentityClient) createIMDSAuthRequest(ctx context.Context, id Ma
q.Add("resource", strings.Join(scopes, " "))
if id != nil {
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
q.Add(msiResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
@ -296,7 +337,7 @@ func (c *managedIdentityClient) createIMDSAuthRequest(ctx context.Context, id Ma
}
func (c *managedIdentityClient) createAppServiceAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
request, err := azruntime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
@ -306,7 +347,7 @@ func (c *managedIdentityClient) createAppServiceAuthRequest(ctx context.Context,
q.Add("resource", scopes[0])
if id != nil {
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
q.Add(miResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
@ -316,7 +357,7 @@ func (c *managedIdentityClient) createAppServiceAuthRequest(ctx context.Context,
}
func (c *managedIdentityClient) createAzureMLAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
request, err := azruntime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
@ -329,7 +370,7 @@ func (c *managedIdentityClient) createAzureMLAuthRequest(ctx context.Context, id
if id.idKind() == miResourceID {
log.Write(EventAuthentication, "WARNING: Azure ML doesn't support specifying a managed identity by resource ID")
q.Set("clientid", "")
q.Set(qpResID, id.String())
q.Set(miResID, id.String())
} else {
q.Set("clientid", id.String())
}
@ -339,7 +380,7 @@ func (c *managedIdentityClient) createAzureMLAuthRequest(ctx context.Context, id
}
func (c *managedIdentityClient) createServiceFabricAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
request, err := azruntime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
@ -351,7 +392,7 @@ func (c *managedIdentityClient) createServiceFabricAuthRequest(ctx context.Conte
if id != nil {
log.Write(EventAuthentication, "WARNING: Service Fabric doesn't support selecting a user-assigned identity at runtime")
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
q.Add(miResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
@ -362,7 +403,7 @@ func (c *managedIdentityClient) createServiceFabricAuthRequest(ctx context.Conte
func (c *managedIdentityClient) getAzureArcSecretKey(ctx context.Context, resources []string) (string, error) {
// create the request to retreive the secret key challenge provided by the HIMDS service
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
request, err := azruntime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return "", err
}
@ -384,22 +425,36 @@ func (c *managedIdentityClient) getAzureArcSecretKey(ctx context.Context, resour
}
header := response.Header.Get("WWW-Authenticate")
if len(header) == 0 {
return "", errors.New("did not receive a value from WWW-Authenticate header")
return "", newAuthenticationFailedError(credNameManagedIdentity, "HIMDS response has no WWW-Authenticate header", nil, nil)
}
// the WWW-Authenticate header is expected in the following format: Basic realm=/some/file/path.key
pos := strings.LastIndex(header, "=")
if pos == -1 {
return "", fmt.Errorf("did not receive a correct value from WWW-Authenticate header: %s", header)
_, p, found := strings.Cut(header, "=")
if !found {
return "", newAuthenticationFailedError(credNameManagedIdentity, "unexpected WWW-Authenticate header from HIMDS: "+header, nil, nil)
}
key, err := os.ReadFile(header[pos+1:])
expected, err := arcKeyDirectory()
if err != nil {
return "", fmt.Errorf("could not read file (%s) contents: %v", header[pos+1:], err)
return "", err
}
if filepath.Dir(p) != expected || !strings.HasSuffix(p, ".key") {
return "", newAuthenticationFailedError(credNameManagedIdentity, "unexpected file path from HIMDS service: "+p, nil, nil)
}
f, err := os.Stat(p)
if err != nil {
return "", newAuthenticationFailedError(credNameManagedIdentity, fmt.Sprintf("could not stat %q: %v", p, err), nil, nil)
}
if s := f.Size(); s > 4096 {
return "", newAuthenticationFailedError(credNameManagedIdentity, fmt.Sprintf("key is too large (%d bytes)", s), nil, nil)
}
key, err := os.ReadFile(p)
if err != nil {
return "", newAuthenticationFailedError(credNameManagedIdentity, fmt.Sprintf("could not read %q: %v", p, err), nil, nil)
}
return string(key), nil
}
func (c *managedIdentityClient) createAzureArcAuthRequest(ctx context.Context, id ManagedIDKind, resources []string, key string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
request, err := azruntime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
@ -411,7 +466,7 @@ func (c *managedIdentityClient) createAzureArcAuthRequest(ctx context.Context, i
if id != nil {
log.Write(EventAuthentication, "WARNING: Azure Arc doesn't support user-assigned managed identities")
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
q.Add(miResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
@ -421,7 +476,7 @@ func (c *managedIdentityClient) createAzureArcAuthRequest(ctx context.Context, i
}
func (c *managedIdentityClient) createCloudShellAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodPost, c.endpoint)
request, err := azruntime.NewRequest(ctx, http.MethodPost, c.endpoint)
if err != nil {
return nil, err
}
@ -437,7 +492,7 @@ func (c *managedIdentityClient) createCloudShellAuthRequest(ctx context.Context,
log.Write(EventAuthentication, "WARNING: Cloud Shell doesn't support user-assigned managed identities")
q := request.Raw().URL.Query()
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
q.Add(miResID, id.String())
} else {
q.Add(qpClientID, id.String())
}

View File

@ -64,12 +64,19 @@ type ManagedIdentityCredentialOptions struct {
// instead of the hosting environment's default. The value may be the identity's client ID or resource ID, but note that
// some platforms don't accept resource IDs.
ID ManagedIDKind
// dac indicates whether the credential is part of DefaultAzureCredential. When true, and the environment doesn't have
// configuration for a specific managed identity API, the credential tries to determine whether IMDS is available before
// sending its first token request. It does this by sending a malformed request with a short timeout. Any response to that
// request is taken to mean IMDS is available, in which case the credential will send ordinary token requests thereafter
// with no special timeout. The purpose of this behavior is to prevent a very long timeout when IMDS isn't available.
dac bool
}
// ManagedIdentityCredential authenticates an Azure managed identity in any hosting environment supporting managed identities.
// This credential authenticates a system-assigned identity by default. Use ManagedIdentityCredentialOptions.ID to specify a
// user-assigned identity. See Microsoft Entra ID documentation for more information about managed identities:
// https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview
// https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview
type ManagedIdentityCredential struct {
client *confidentialClient
mic *managedIdentityClient

View File

@ -10,6 +10,7 @@ import (
"context"
"crypto"
"crypto/x509"
"errors"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
@ -24,7 +25,7 @@ const credNameOBO = "OnBehalfOfCredential"
// is not an interactive authentication flow, an application using it must have admin consent for any delegated
// permissions before requesting tokens for them. See [Microsoft Entra ID documentation] for more details.
//
// [Microsoft Entra ID documentation]: https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow
// [Microsoft Entra ID documentation]: https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow
type OnBehalfOfCredential struct {
client *confidentialClient
}
@ -60,6 +61,19 @@ func NewOnBehalfOfCredentialWithCertificate(tenantID, clientID, userAssertion st
return newOnBehalfOfCredential(tenantID, clientID, userAssertion, cred, options)
}
// NewOnBehalfOfCredentialWithClientAssertions constructs an OnBehalfOfCredential that authenticates with client assertions.
// userAssertion is the user's access token for the application. The getAssertion function should return client assertions
// that authenticate the application to Microsoft Entra ID, such as federated credentials.
func NewOnBehalfOfCredentialWithClientAssertions(tenantID, clientID, userAssertion string, getAssertion func(context.Context) (string, error), options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
if getAssertion == nil {
return nil, errors.New("getAssertion can't be nil. It must be a function that returns client assertions")
}
cred := confidential.NewCredFromAssertionCallback(func(ctx context.Context, _ confidential.AssertionRequestOptions) (string, error) {
return getAssertion(ctx)
})
return newOnBehalfOfCredential(tenantID, clientID, userAssertion, cred, options)
}
// NewOnBehalfOfCredentialWithSecret constructs an OnBehalfOfCredential that authenticates with a client secret.
func NewOnBehalfOfCredentialWithSecret(tenantID, clientID, userAssertion, clientSecret string, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
cred, err := confidential.NewCredFromSecret(clientSecret)

View File

@ -152,7 +152,7 @@ func (p *publicClient) GetToken(ctx context.Context, tro policy.TokenRequestOpti
return p.token(ar, err)
}
if p.opts.DisableAutomaticAuthentication {
return azcore.AccessToken{}, errAuthenticationRequired
return azcore.AccessToken{}, newauthenticationRequiredError(p.name, tro)
}
at, err := p.reqToken(ctx, client, tro)
if err == nil {

View File

@ -0,0 +1,112 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
# IMPORTANT: Do not invoke this file directly. Please instead run eng/common/TestResources/New-TestResources.ps1 from the repository root.
param (
[hashtable] $AdditionalParameters = @{},
[hashtable] $DeploymentOutputs
)
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
if ($CI) {
if (!$AdditionalParameters['deployResources']) {
Write-Host "Skipping post-provisioning script because resources weren't deployed"
return
}
az login --service-principal -u $DeploymentOutputs['AZIDENTITY_CLIENT_ID'] -p $DeploymentOutputs['AZIDENTITY_CLIENT_SECRET'] --tenant $DeploymentOutputs['AZIDENTITY_TENANT_ID']
az account set --subscription $DeploymentOutputs['AZIDENTITY_SUBSCRIPTION_ID']
}
Write-Host "Building container"
$image = "$($DeploymentOutputs['AZIDENTITY_ACR_LOGIN_SERVER'])/azidentity-managed-id-test"
Set-Content -Path "$PSScriptRoot/Dockerfile" -Value @"
FROM mcr.microsoft.com/oss/go/microsoft/golang:latest as builder
ENV GOARCH=amd64 GOWORK=off
COPY . /azidentity
WORKDIR /azidentity/testdata/managed-id-test
RUN go mod tidy
RUN go build -o /build/managed-id-test .
RUN GOOS=windows go build -o /build/managed-id-test.exe .
FROM mcr.microsoft.com/mirror/docker/library/alpine:3.16
RUN apk add gcompat
COPY --from=builder /build/* .
RUN chmod +x managed-id-test
CMD ["./managed-id-test"]
"@
# build from sdk/azidentity because we need that dir in the context (because the test app uses local azidentity)
docker build -t $image "$PSScriptRoot"
az acr login -n $DeploymentOutputs['AZIDENTITY_ACR_NAME']
docker push $image
$rg = $DeploymentOutputs['AZIDENTITY_RESOURCE_GROUP']
# ACI is easier to provision here than in the bicep file because the image isn't available before now
Write-Host "Deploying Azure Container Instance"
$aciName = "azidentity-test"
az container create -g $rg -n $aciName --image $image `
--acr-identity $($DeploymentOutputs['AZIDENTITY_USER_ASSIGNED_IDENTITY']) `
--assign-identity [system] $($DeploymentOutputs['AZIDENTITY_USER_ASSIGNED_IDENTITY']) `
--role "Storage Blob Data Reader" `
--scope $($DeploymentOutputs['AZIDENTITY_STORAGE_ID']) `
-e AZIDENTITY_STORAGE_NAME=$($DeploymentOutputs['AZIDENTITY_STORAGE_NAME']) `
AZIDENTITY_STORAGE_NAME_USER_ASSIGNED=$($DeploymentOutputs['AZIDENTITY_STORAGE_NAME_USER_ASSIGNED']) `
AZIDENTITY_USER_ASSIGNED_IDENTITY=$($DeploymentOutputs['AZIDENTITY_USER_ASSIGNED_IDENTITY']) `
FUNCTIONS_CUSTOMHANDLER_PORT=80
Write-Host "##vso[task.setvariable variable=AZIDENTITY_ACI_NAME;]$aciName"
# Azure Functions deployment: copy the Windows binary from the Docker image, deploy it in a zip
Write-Host "Deploying to Azure Functions"
$container = docker create $image
docker cp ${container}:managed-id-test.exe "$PSScriptRoot/testdata/managed-id-test/"
docker rm -v $container
Compress-Archive -Path "$PSScriptRoot/testdata/managed-id-test/*" -DestinationPath func.zip -Force
az functionapp deploy -g $rg -n $DeploymentOutputs['AZIDENTITY_FUNCTION_NAME'] --src-path func.zip --type zip
Write-Host "Creating federated identity"
$aksName = $DeploymentOutputs['AZIDENTITY_AKS_NAME']
$idName = $DeploymentOutputs['AZIDENTITY_USER_ASSIGNED_IDENTITY_NAME']
$issuer = az aks show -g $rg -n $aksName --query "oidcIssuerProfile.issuerUrl" -otsv
$podName = "azidentity-test"
$serviceAccountName = "workload-identity-sa"
az identity federated-credential create -g $rg --identity-name $idName --issuer $issuer --name $idName --subject system:serviceaccount:default:$serviceAccountName
Write-Host "Deploying to AKS"
az aks get-credentials -g $rg -n $aksName
az aks update --attach-acr $DeploymentOutputs['AZIDENTITY_ACR_NAME'] -g $rg -n $aksName
Set-Content -Path "$PSScriptRoot/k8s.yaml" -Value @"
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
azure.workload.identity/client-id: $($DeploymentOutputs['AZIDENTITY_USER_ASSIGNED_IDENTITY_CLIENT_ID'])
name: $serviceAccountName
namespace: default
---
apiVersion: v1
kind: Pod
metadata:
name: $podName
namespace: default
labels:
app: $podName
azure.workload.identity/use: "true"
spec:
serviceAccountName: $serviceAccountName
containers:
- name: $podName
image: $image
env:
- name: AZIDENTITY_STORAGE_NAME
value: $($DeploymentOutputs['AZIDENTITY_STORAGE_NAME_USER_ASSIGNED'])
- name: AZIDENTITY_USE_WORKLOAD_IDENTITY
value: "true"
- name: FUNCTIONS_CUSTOMHANDLER_PORT
value: "80"
nodeSelector:
kubernetes.io/os: linux
"@
kubectl apply -f "$PSScriptRoot/k8s.yaml"
Write-Host "##vso[task.setvariable variable=AZIDENTITY_POD_NAME;]$podName"

View File

@ -1,36 +1,44 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
# IMPORTANT: Do not invoke this file directly. Please instead run eng/common/TestResources/New-TestResources.ps1 from the repository root.
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
param (
[hashtable] $AdditionalParameters = @{},
# Captures any arguments from eng/New-TestResources.ps1 not declared here (no parameter errors).
[Parameter(ValueFromRemainingArguments = $true)]
$RemainingArguments
)
if (-not (Test-Path "$PSScriptRoot/sshkey.pub")) {
ssh-keygen -t rsa -b 4096 -f "$PSScriptRoot/sshkey" -N '' -C ''
}
$templateFileParameters['sshPubKey'] = Get-Content "$PSScriptRoot/sshkey.pub"
if (!$CI) {
# TODO: Remove this once auto-cloud config downloads are supported locally
Write-Host "Skipping cert setup in local testing mode"
return
}
if ($EnvironmentVariables -eq $null -or $EnvironmentVariables.Count -eq 0) {
if ($null -eq $EnvironmentVariables -or $EnvironmentVariables.Count -eq 0) {
throw "EnvironmentVariables must be set in the calling script New-TestResources.ps1"
}
$tmp = $env:TEMP ? $env:TEMP : [System.IO.Path]::GetTempPath()
$pfxPath = Join-Path $tmp "test.pfx"
$pemPath = Join-Path $tmp "test.pem"
$sniPath = Join-Path $tmp "testsni.pfx"
Write-Host "Creating identity test files: $pfxPath $pemPath $sniPath"
Write-Host "Creating identity test files: $pfxPath $pemPath"
[System.Convert]::FromBase64String($EnvironmentVariables['PFX_CONTENTS']) | Set-Content -Path $pfxPath -AsByteStream
Set-Content -Path $pemPath -Value $EnvironmentVariables['PEM_CONTENTS']
[System.Convert]::FromBase64String($EnvironmentVariables['SNI_CONTENTS']) | Set-Content -Path $sniPath -AsByteStream
# Set for pipeline
Write-Host "##vso[task.setvariable variable=IDENTITY_SP_CERT_PFX;]$pfxPath"
Write-Host "##vso[task.setvariable variable=IDENTITY_SP_CERT_PEM;]$pemPath"
Write-Host "##vso[task.setvariable variable=IDENTITY_SP_CERT_SNI;]$sniPath"
# Set for local
$env:IDENTITY_SP_CERT_PFX = $pfxPath
$env:IDENTITY_SP_CERT_PEM = $pemPath
$env:IDENTITY_SP_CERT_SNI = $sniPath

View File

@ -1 +1,219 @@
param baseName string
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
@description('Kubernetes cluster admin user name.')
param adminUser string = 'azureuser'
@minLength(6)
@maxLength(23)
@description('The base resource name.')
param baseName string = resourceGroup().name
@description('Whether to deploy resources. When set to false, this file deploys nothing.')
param deployResources bool = false
param sshPubKey string = ''
@description('The location of the resource. By default, this is the same as the resource group.')
param location string = resourceGroup().location
// https://learn.microsoft.com/azure/role-based-access-control/built-in-roles
var acrPull = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
var blobReader = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')
resource sa 'Microsoft.Storage/storageAccounts@2021-08-01' = if (deployResources) {
kind: 'StorageV2'
location: location
name: 'sa${uniqueString(baseName)}'
properties: {
accessTier: 'Hot'
}
sku: {
name: 'Standard_LRS'
}
}
resource saUserAssigned 'Microsoft.Storage/storageAccounts@2021-08-01' = if (deployResources) {
kind: 'StorageV2'
location: location
name: 'sa2${uniqueString(baseName)}'
properties: {
accessTier: 'Hot'
}
sku: {
name: 'Standard_LRS'
}
}
resource usermgdid 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = if (deployResources) {
location: location
name: baseName
}
resource acrPullContainerInstance 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (deployResources) {
name: guid(resourceGroup().id, acrPull, 'containerInstance')
properties: {
principalId: deployResources ? usermgdid.properties.principalId : ''
principalType: 'ServicePrincipal'
roleDefinitionId: acrPull
}
scope: containerRegistry
}
resource blobRoleUserAssigned 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (deployResources) {
scope: saUserAssigned
name: guid(resourceGroup().id, blobReader, usermgdid.id)
properties: {
principalId: deployResources ? usermgdid.properties.principalId : ''
principalType: 'ServicePrincipal'
roleDefinitionId: blobReader
}
}
resource blobRoleFunc 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (deployResources) {
name: guid(resourceGroup().id, blobReader, 'azfunc')
properties: {
principalId: deployResources ? azfunc.identity.principalId : ''
roleDefinitionId: blobReader
principalType: 'ServicePrincipal'
}
scope: sa
}
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = if (deployResources) {
location: location
name: uniqueString(resourceGroup().id)
properties: {
adminUserEnabled: true
}
sku: {
name: 'Basic'
}
}
resource farm 'Microsoft.Web/serverfarms@2021-03-01' = if (deployResources) {
kind: 'app'
location: location
name: '${baseName}_asp'
properties: {}
sku: {
capacity: 1
family: 'B'
name: 'B1'
size: 'B1'
tier: 'Basic'
}
}
resource azfunc 'Microsoft.Web/sites@2021-03-01' = if (deployResources) {
identity: {
type: 'SystemAssigned, UserAssigned'
userAssignedIdentities: {
'${deployResources ? usermgdid.id : ''}': {}
}
}
kind: 'functionapp'
location: location
name: '${baseName}func'
properties: {
enabled: true
httpsOnly: true
keyVaultReferenceIdentity: 'SystemAssigned'
serverFarmId: farm.id
siteConfig: {
alwaysOn: true
appSettings: [
{
name: 'AZIDENTITY_STORAGE_NAME'
value: deployResources ? sa.name : null
}
{
name: 'AZIDENTITY_STORAGE_NAME_USER_ASSIGNED'
value: deployResources ? saUserAssigned.name : null
}
{
name: 'AZIDENTITY_USER_ASSIGNED_IDENTITY'
value: deployResources ? usermgdid.id : null
}
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${deployResources ? sa.name : ''};EndpointSuffix=${deployResources ? environment().suffixes.storage : ''};AccountKey=${deployResources ? sa.listKeys().keys[0].value : ''}'
}
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'custom'
}
{
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
value: 'DefaultEndpointsProtocol=https;AccountName=${deployResources ? sa.name : ''};EndpointSuffix=${deployResources ? environment().suffixes.storage : ''};AccountKey=${deployResources ? sa.listKeys().keys[0].value : ''}'
}
{
name: 'WEBSITE_CONTENTSHARE'
value: toLower('${baseName}-func')
}
]
http20Enabled: true
minTlsVersion: '1.2'
}
}
}
resource aks 'Microsoft.ContainerService/managedClusters@2023-06-01' = if (deployResources) {
name: baseName
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
agentPoolProfiles: [
{
count: 1
enableAutoScaling: false
kubeletDiskType: 'OS'
mode: 'System'
name: 'agentpool'
osDiskSizeGB: 128
osDiskType: 'Managed'
osSKU: 'Ubuntu'
osType: 'Linux'
type: 'VirtualMachineScaleSets'
vmSize: 'Standard_D2s_v3'
}
]
dnsPrefix: 'identitytest'
enableRBAC: true
linuxProfile: {
adminUsername: adminUser
ssh: {
publicKeys: [
{
keyData: sshPubKey
}
]
}
}
oidcIssuerProfile: {
enabled: true
}
securityProfile: {
workloadIdentity: {
enabled: true
}
}
}
}
output AZIDENTITY_ACR_LOGIN_SERVER string = deployResources ? containerRegistry.properties.loginServer : ''
output AZIDENTITY_ACR_NAME string = deployResources ? containerRegistry.name : ''
output AZIDENTITY_AKS_NAME string = deployResources ? aks.name : ''
output AZIDENTITY_FUNCTION_NAME string = deployResources ? azfunc.name : ''
output AZIDENTITY_STORAGE_ID string = deployResources ? sa.id : ''
output AZIDENTITY_STORAGE_NAME string = deployResources ? sa.name : ''
output AZIDENTITY_STORAGE_NAME_USER_ASSIGNED string = deployResources ? saUserAssigned.name : ''
output AZIDENTITY_USER_ASSIGNED_IDENTITY string = deployResources ? usermgdid.id : ''
output AZIDENTITY_USER_ASSIGNED_IDENTITY_CLIENT_ID string = deployResources ? usermgdid.properties.clientId : ''
output AZIDENTITY_USER_ASSIGNED_IDENTITY_NAME string = deployResources ? usermgdid.name : ''

View File

@ -14,5 +14,5 @@ const (
module = "github.com/Azure/azure-sdk-for-go/sdk/" + component
// Version is the semantic version (see http://semver.org) of this module.
version = "v1.5.1"
version = "v1.6.0"
)

View File

@ -39,6 +39,11 @@ type PayloadOptions struct {
// Subsequent reads will access the cached value.
// Exported as runtime.Payload() WITHOUT the opts parameter.
func Payload(resp *http.Response, opts *PayloadOptions) ([]byte, error) {
if resp.Body == nil {
// this shouldn't happen in real-world scenarios as a
// response with no body should set it to http.NoBody
return nil, nil
}
modifyBytes := func(b []byte) []byte { return b }
if opts != nil && opts.BytesModifier != nil {
modifyBytes = opts.BytesModifier

View File

@ -48,8 +48,8 @@ duplication.
.Net People, Take note on X509:
This uses x509.Certificates and private keys. x509 does not store private keys. .Net
has some x509.Certificate2 thing that has private keys, but that is just some bullcrap that .Net
added, it doesn't exist in real life. As such I've put a PEM decoder into here.
has a x509.Certificate2 abstraction that has private keys, but that just a strange invention.
As such I've put a PEM decoder into here.
*/
// TODO(msal): This should have example code for each method on client using Go's example doc framework.

View File

@ -217,11 +217,13 @@ func WithClaims(claims string) interface {
func WithAuthenticationScheme(authnScheme AuthenticationScheme) interface {
AcquireSilentOption
AcquireInteractiveOption
AcquireByUsernamePasswordOption
options.CallOption
} {
return struct {
AcquireSilentOption
AcquireInteractiveOption
AcquireByUsernamePasswordOption
options.CallOption
}{
CallOption: options.NewCallOption(
@ -231,6 +233,8 @@ func WithAuthenticationScheme(authnScheme AuthenticationScheme) interface {
t.authnScheme = authnScheme
case *interactiveAuthOptions:
t.authnScheme = authnScheme
case *acquireTokenByUsernamePasswordOptions:
t.authnScheme = authnScheme
default:
return fmt.Errorf("unexpected options type %T", a)
}
@ -349,6 +353,7 @@ func (pca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts
// acquireTokenByUsernamePasswordOptions contains optional configuration for AcquireTokenByUsernamePassword
type acquireTokenByUsernamePasswordOptions struct {
claims, tenantID string
authnScheme AuthenticationScheme
}
// AcquireByUsernamePasswordOption is implemented by options for AcquireTokenByUsernamePassword
@ -374,6 +379,9 @@ func (pca Client) AcquireTokenByUsernamePassword(ctx context.Context, scopes []s
authParams.Claims = o.claims
authParams.Username = username
authParams.Password = password
if o.authnScheme != nil {
authParams.AuthnScheme = o.authnScheme
}
token, err := pca.base.Token.UsernamePassword(ctx, authParams)
if err != nil {

View File

@ -62,7 +62,7 @@ func (m *SigningMethodECDSA) Verify(signingString string, sig []byte, key interf
case *ecdsa.PublicKey:
ecdsaKey = k
default:
return newError("ECDSA verify expects *ecsda.PublicKey", ErrInvalidKeyType)
return newError("ECDSA verify expects *ecdsa.PublicKey", ErrInvalidKeyType)
}
if len(sig) != 2*m.KeySize {
@ -96,7 +96,7 @@ func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) ([]byte
case *ecdsa.PrivateKey:
ecdsaKey = k
default:
return nil, newError("ECDSA sign expects *ecsda.PrivateKey", ErrInvalidKeyType)
return nil, newError("ECDSA sign expects *ecdsa.PrivateKey", ErrInvalidKeyType)
}
// Create the hasher

View File

@ -91,7 +91,7 @@ func (m *SigningMethodHMAC) Verify(signingString string, sig []byte, key interfa
func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) ([]byte, error) {
if keyBytes, ok := key.([]byte); ok {
if !m.Hash.Available() {
return nil, newError("HMAC sign expects []byte", ErrInvalidKeyType)
return nil, ErrHashUnavailable
}
hasher := hmac.New(m.Hash.New, keyBytes)
@ -100,5 +100,5 @@ func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) ([]byte,
return hasher.Sum(nil), nil
}
return nil, ErrInvalidKeyType
return nil, newError("HMAC sign expects []byte", ErrInvalidKeyType)
}

10
vendor/modules.txt vendored
View File

@ -1,4 +1,4 @@
# github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1
# github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1
## explicit; go 1.18
github.com/Azure/azure-sdk-for-go/sdk/azcore
github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/resource
@ -20,11 +20,11 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime
github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming
github.com/Azure/azure-sdk-for-go/sdk/azcore/to
github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing
# github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1
# github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0
## explicit; go 1.18
github.com/Azure/azure-sdk-for-go/sdk/azidentity
github.com/Azure/azure-sdk-for-go/sdk/azidentity/internal
# github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1
# github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0
## explicit; go 1.18
github.com/Azure/azure-sdk-for-go/sdk/internal/diag
github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo
@ -39,7 +39,7 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets
# github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0
## explicit; go 1.18
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal
# github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1
# github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2
## explicit; go 1.18
github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache
github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential
@ -307,7 +307,7 @@ github.com/gogo/protobuf/gogoproto
github.com/gogo/protobuf/proto
github.com/gogo/protobuf/protoc-gen-gogo/descriptor
github.com/gogo/protobuf/sortkeys
# github.com/golang-jwt/jwt/v5 v5.2.0
# github.com/golang-jwt/jwt/v5 v5.2.1
## explicit; go 1.18
github.com/golang-jwt/jwt/v5
# github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da