Skip to content

Commit d732f7c

Browse files
authored
Merge azcore beta branch to main (Azure#20446)
1 parent c18acc0 commit d732f7c

File tree

8 files changed

+239
-71
lines changed

8 files changed

+239
-71
lines changed

sdk/azcore/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Release History
22

3-
## 1.4.1 (Unreleased)
3+
## 1.5.0 (Unreleased)
44

55
### Features Added
66

@@ -11,6 +11,11 @@
1111

1212
### Other Changes
1313

14+
## 1.5.0-beta.1 (2023-03-02)
15+
16+
### Features Added
17+
* This release includes the features added in v1.4.0-beta.1
18+
1419
## 1.4.0 (2023-03-02)
1520
> This release doesn't include features added in v1.4.0-beta.1. They will return in v1.5.0-beta.1.
1621

sdk/azcore/arm/policy/policy.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import (
1414

1515
// BearerTokenOptions configures the bearer token policy's behavior.
1616
type BearerTokenOptions struct {
17+
// AuxiliaryTenants are additional tenant IDs for authenticating cross-tenant requests.
18+
// The policy will add a token from each of these tenants to every request. The
19+
// authenticating user or service principal must be a guest in these tenants, and the
20+
// policy's credential must support multitenant authentication.
21+
AuxiliaryTenants []string
22+
1723
// Scopes contains the list of permission scopes required for the token.
1824
Scopes []string
1925
}
@@ -44,6 +50,12 @@ type RegistrationOptions struct {
4450
type ClientOptions struct {
4551
policy.ClientOptions
4652

53+
// AuxiliaryTenants are additional tenant IDs for authenticating cross-tenant requests.
54+
// The client will add a token from each of these tenants to every request. The
55+
// authenticating user or service principal must be a guest in these tenants, and the
56+
// client's credential must support multitenant authentication.
57+
AuxiliaryTenants []string
58+
4759
// DisableRPRegistration disables the auto-RP registration policy. Defaults to false.
4860
DisableRPRegistration bool
4961
}

sdk/azcore/arm/runtime/pipeline.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ func NewPipeline(module, version string, cred azcore.TokenCredential, plOpts azr
2828
if err != nil {
2929
return azruntime.Pipeline{}, err
3030
}
31-
authPolicy := NewBearerTokenPolicy(cred, &armpolicy.BearerTokenOptions{Scopes: []string{conf.Audience + "/.default"}})
31+
authPolicy := NewBearerTokenPolicy(cred, &armpolicy.BearerTokenOptions{
32+
AuxiliaryTenants: options.AuxiliaryTenants,
33+
Scopes: []string{conf.Audience + "/.default"},
34+
})
3235
perRetry := make([]azpolicy.Policy, len(plOpts.PerRetry), len(plOpts.PerRetry)+1)
3336
copy(perRetry, plOpts.PerRetry)
3437
plOpts.PerRetry = append(perRetry, authPolicy)

sdk/azcore/arm/runtime/policy_bearer_token.go

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package runtime
55

66
import (
77
"context"
8+
"encoding/base64"
89
"fmt"
910
"net/http"
1011
"strings"
@@ -14,9 +15,13 @@ import (
1415
armpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
1516
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
1617
azpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
18+
azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
1719
"github.com/Azure/azure-sdk-for-go/sdk/internal/temporal"
1820
)
1921

22+
const headerAuxiliaryAuthorization = "x-ms-authorization-auxiliary"
23+
24+
// acquiringResourceState holds data for an auxiliary token request
2025
type acquiringResourceState struct {
2126
ctx context.Context
2227
p *BearerTokenPolicy
@@ -26,7 +31,10 @@ type acquiringResourceState struct {
2631
// acquire acquires or updates the resource; only one
2732
// thread/goroutine at a time ever calls this function
2833
func acquire(state acquiringResourceState) (newResource azcore.AccessToken, newExpiration time.Time, err error) {
29-
tk, err := state.p.cred.GetToken(state.ctx, azpolicy.TokenRequestOptions{Scopes: state.p.options.Scopes})
34+
tk, err := state.p.cred.GetToken(state.ctx, azpolicy.TokenRequestOptions{
35+
Scopes: state.p.scopes,
36+
TenantID: state.tenant,
37+
})
3038
if err != nil {
3139
return azcore.AccessToken{}, time.Time{}, err
3240
}
@@ -35,13 +43,10 @@ func acquire(state acquiringResourceState) (newResource azcore.AccessToken, newE
3543

3644
// BearerTokenPolicy authorizes requests with bearer tokens acquired from a TokenCredential.
3745
type BearerTokenPolicy struct {
38-
// mainResource is the resource to be retreived using the tenant specified in the credential
39-
mainResource *temporal.Resource[azcore.AccessToken, acquiringResourceState]
40-
// auxResources are additional resources that are required for cross-tenant applications
4146
auxResources map[string]*temporal.Resource[azcore.AccessToken, acquiringResourceState]
42-
// the following fields are read-only
43-
cred azcore.TokenCredential
44-
options armpolicy.BearerTokenOptions
47+
btp *azruntime.BearerTokenPolicy
48+
cred azcore.TokenCredential
49+
scopes []string
4550
}
4651

4752
// NewBearerTokenPolicy creates a policy object that authorizes requests with bearer tokens.
@@ -51,36 +56,90 @@ func NewBearerTokenPolicy(cred azcore.TokenCredential, opts *armpolicy.BearerTok
5156
if opts == nil {
5257
opts = &armpolicy.BearerTokenOptions{}
5358
}
54-
p := &BearerTokenPolicy{
55-
cred: cred,
56-
options: *opts,
57-
mainResource: temporal.NewResource(acquire),
59+
p := &BearerTokenPolicy{cred: cred}
60+
p.auxResources = make(map[string]*temporal.Resource[azcore.AccessToken, acquiringResourceState], len(opts.AuxiliaryTenants))
61+
for _, t := range opts.AuxiliaryTenants {
62+
p.auxResources[t] = temporal.NewResource(acquire)
5863
}
64+
p.scopes = make([]string, len(opts.Scopes))
65+
copy(p.scopes, opts.Scopes)
66+
p.btp = azruntime.NewBearerTokenPolicy(cred, opts.Scopes, &azpolicy.BearerTokenOptions{
67+
AuthorizationHandler: azpolicy.AuthorizationHandler{
68+
OnChallenge: p.onChallenge,
69+
OnRequest: p.onRequest,
70+
},
71+
})
5972
return p
6073
}
6174

62-
// Do authorizes a request with a bearer token
63-
func (b *BearerTokenPolicy) Do(req *azpolicy.Request) (*http.Response, error) {
75+
func (b *BearerTokenPolicy) onChallenge(req *azpolicy.Request, res *http.Response, authNZ func(azpolicy.TokenRequestOptions) error) error {
76+
challenge := res.Header.Get(shared.HeaderWWWAuthenticate)
77+
claims, err := parseChallenge(challenge)
78+
if err != nil {
79+
// the challenge contains claims we can't parse
80+
return err
81+
} else if claims != "" {
82+
// request a new token having the specified claims, send the request again
83+
return authNZ(azpolicy.TokenRequestOptions{Claims: claims, Scopes: b.scopes})
84+
}
85+
// auth challenge didn't include claims, so this is a simple authorization failure
86+
return azruntime.NewResponseError(res)
87+
}
88+
89+
// onRequest authorizes requests with one or more bearer tokens
90+
func (b *BearerTokenPolicy) onRequest(req *azpolicy.Request, authNZ func(azpolicy.TokenRequestOptions) error) error {
91+
// authorize the request with a token for the primary tenant
92+
err := authNZ(azpolicy.TokenRequestOptions{Scopes: b.scopes})
93+
if err != nil || len(b.auxResources) == 0 {
94+
return err
95+
}
96+
// add tokens for auxiliary tenants
6497
as := acquiringResourceState{
6598
ctx: req.Raw().Context(),
6699
p: b,
67100
}
68-
tk, err := b.mainResource.Get(as)
69-
if err != nil {
70-
return nil, err
71-
}
72-
req.Raw().Header.Set(shared.HeaderAuthorization, shared.BearerTokenPrefix+tk.Token)
73-
auxTokens := []string{}
101+
auxTokens := make([]string, 0, len(b.auxResources))
74102
for tenant, er := range b.auxResources {
75103
as.tenant = tenant
76104
auxTk, err := er.Get(as)
77105
if err != nil {
78-
return nil, err
106+
return err
79107
}
80108
auxTokens = append(auxTokens, fmt.Sprintf("%s%s", shared.BearerTokenPrefix, auxTk.Token))
81109
}
82-
if len(auxTokens) > 0 {
83-
req.Raw().Header.Set(shared.HeaderAuxiliaryAuthorization, strings.Join(auxTokens, ", "))
110+
req.Raw().Header.Set(headerAuxiliaryAuthorization, strings.Join(auxTokens, ", "))
111+
return nil
112+
}
113+
114+
// Do authorizes a request with a bearer token
115+
func (b *BearerTokenPolicy) Do(req *azpolicy.Request) (*http.Response, error) {
116+
return b.btp.Do(req)
117+
}
118+
119+
// parseChallenge parses claims from an authentication challenge issued by ARM so a client can request a token
120+
// that will satisfy conditional access policies. It returns a non-nil error when the given value contains
121+
// claims it can't parse. If the value contains no claims, it returns an empty string and a nil error.
122+
func parseChallenge(wwwAuthenticate string) (string, error) {
123+
claims := ""
124+
var err error
125+
for _, param := range strings.Split(wwwAuthenticate, ",") {
126+
if _, after, found := strings.Cut(param, "claims="); found {
127+
if claims != "" {
128+
// The header contains multiple challenges, at least two of which specify claims. The specs allow this
129+
// but it's unclear what a client should do in this case and there's as yet no concrete example of it.
130+
err = fmt.Errorf("found multiple claims challenges in %q", wwwAuthenticate)
131+
break
132+
}
133+
// trim stuff that would get an error from RawURLEncoding; claims may or may not be padded
134+
claims = strings.Trim(after, `\"=`)
135+
// we don't return this error because it's something unhelpful like "illegal base64 data at input byte 42"
136+
if b, decErr := base64.RawURLEncoding.DecodeString(claims); decErr == nil {
137+
claims = string(b)
138+
} else {
139+
err = fmt.Errorf("failed to parse claims from %q", wwwAuthenticate)
140+
break
141+
}
142+
}
84143
}
85-
return req.Next()
144+
return claims, err
86145
}

0 commit comments

Comments
 (0)