Skip to content

Commit fe5f262

Browse files
committed
Added withAttribute for AcquireTokenByCredential
1 parent cb3bf1b commit fe5f262

File tree

3 files changed

+238
-20
lines changed

3 files changed

+238
-20
lines changed

apps/confidential/confidential.go

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -550,11 +550,9 @@ func WithTenantID(tenantID string) interface {
550550
// acquireTokenSilentOptions are all the optional settings to an AcquireTokenSilent() call.
551551
// These are set by using various AcquireTokenSilentOption functions.
552552
type acquireTokenSilentOptions struct {
553-
account Account
554-
claims, tenantID string
555-
authnScheme AuthenticationScheme
556-
extraBodyParameters map[string]string
557-
cacheKeyComponents map[string]string
553+
account Account
554+
claims, tenantID string
555+
authnScheme AuthenticationScheme
558556
}
559557

560558
// AcquireSilentOption is implemented by options for AcquireTokenSilent
@@ -587,7 +585,7 @@ func WithSilentAccount(account Account) interface {
587585

588586
// AcquireTokenSilent acquires a token from either the cache or using a refresh token.
589587
//
590-
// Options: [WithClaims], [WithSilentAccount], [WithTenantID], [WithFMIPath]
588+
// Options: [WithClaims], [WithSilentAccount], [WithTenantID]
591589
func (cca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts ...AcquireSilentOption) (AuthResult, error) {
592590
o := acquireTokenSilentOptions{}
593591
if err := options.ApplyOptions(&o, opts); err != nil {
@@ -604,16 +602,14 @@ func (cca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts
604602
}
605603

606604
silentParameters := base.AcquireTokenSilentParameters{
607-
Scopes: scopes,
608-
Account: o.account,
609-
RequestType: accesstokens.ATConfidential,
610-
Credential: cca.cred,
611-
IsAppCache: o.account.IsZero(),
612-
TenantID: o.tenantID,
613-
AuthnScheme: o.authnScheme,
614-
Claims: o.claims,
615-
ExtraBodyParameters: o.extraBodyParameters,
616-
CacheKeyComponents: o.cacheKeyComponents,
605+
Scopes: scopes,
606+
Account: o.account,
607+
RequestType: accesstokens.ATConfidential,
608+
Credential: cca.cred,
609+
IsAppCache: o.account.IsZero(),
610+
TenantID: o.tenantID,
611+
AuthnScheme: o.authnScheme,
612+
Claims: o.claims,
617613
}
618614

619615
return cca.acquireTokenSilentInternal(ctx, silentParameters)
@@ -737,7 +733,7 @@ type AcquireByCredentialOption interface {
737733

738734
// AcquireTokenByCredential acquires a security token from the authority, using the client credentials grant.
739735
//
740-
// Options: [WithClaims], [WithTenantID], [WithFMIPath]]
736+
// Options: [WithClaims], [WithTenantID], [WithFMIPath], [WithAttribute]
741737
func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string, opts ...AcquireByCredentialOption) (AuthResult, error) {
742738
o := acquireTokenByCredentialOptions{}
743739
err := options.ApplyOptions(&o, opts)
@@ -829,7 +825,7 @@ func (cca Client) RemoveAccount(ctx context.Context, account Account) error {
829825
//
830826
// Example:
831827
//
832-
// result, err := client.AcquireTokenByCredential(ctx, scopes, confidential.WithFMIPath("fmit/path"))
828+
// result, err := client.AcquireTokenByCredential(ctx, scopes, confidential.WithFMIPath("fmi/path"))
833829
func WithFMIPath(path string) interface {
834830
AcquireByCredentialOption
835831
options.CallOption
@@ -842,8 +838,47 @@ func WithFMIPath(path string) interface {
842838
func(a any) error {
843839
switch t := a.(type) {
844840
case *acquireTokenByCredentialOptions:
845-
t.cacheKeyComponents = map[string]string{"fmi_path": path}
846-
t.extraBodyParameters = map[string]string{"fmi_path": path}
841+
if t.extraBodyParameters == nil {
842+
t.extraBodyParameters = make(map[string]string)
843+
}
844+
if t.cacheKeyComponents == nil {
845+
t.cacheKeyComponents = make(map[string]string)
846+
}
847+
t.cacheKeyComponents["fmi_path"] = path
848+
t.extraBodyParameters["fmi_path"] = path
849+
default:
850+
return fmt.Errorf("unexpected options type %T", a)
851+
}
852+
return nil
853+
},
854+
),
855+
}
856+
}
857+
858+
// WithAttribute provies the attribute which is passed on as "xmc_attr" in the token request body.
859+
// This is used in conjunction with federated managed identities to specify the identity attribute
860+
//
861+
// Example:
862+
//
863+
// Using raw string literal (backticks) for easier formatting
864+
// attrs := `{"FavoriteColor": "Blue", "file:/c/users/foobar/documents/info.txt": "{\"permissions\":[\"read\",\"write\"]}"}`
865+
// result, err := client.AcquireTokenByCredential(ctx, scopes, confidential.WithAttribute(attrs))
866+
func WithAttribute(attrValue string) interface {
867+
AcquireByCredentialOption
868+
options.CallOption
869+
} {
870+
return struct {
871+
AcquireByCredentialOption
872+
options.CallOption
873+
}{
874+
CallOption: options.NewCallOption(
875+
func(a any) error {
876+
switch t := a.(type) {
877+
case *acquireTokenByCredentialOptions:
878+
if t.extraBodyParameters == nil {
879+
t.extraBodyParameters = make(map[string]string)
880+
}
881+
t.extraBodyParameters["attributes"] = attrValue
847882
default:
848883
return fmt.Errorf("unexpected options type %T", a)
849884
}

apps/confidential/confidential_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2065,3 +2065,160 @@ func TestFMICacheIsolation(t *testing.T) {
20652065
t.Fatalf("Expected 2 cache entries (1 regular + 1 FMI), got %d", len(cache))
20662066
}
20672067
}
2068+
2069+
// TestWithAttribute validates that the WithAttribute option correctly passes
2070+
// the attribute value to extraBodyParameters in the token request
2071+
func TestWithAttribute(t *testing.T) {
2072+
tests := []struct {
2073+
name string
2074+
attrValue string
2075+
expectInBody bool
2076+
expectedValue string
2077+
}{
2078+
{
2079+
name: "simple attribute",
2080+
attrValue: `{"FavoriteColor":"Blue"}`,
2081+
expectInBody: true,
2082+
expectedValue: `{"FavoriteColor":"Blue"}`,
2083+
},
2084+
{
2085+
name: "nested JSON attribute",
2086+
attrValue: `{"FavoriteColor": "Blue", "file:/c/users/foobar/documents/info.txt": "{\"permissions\":[\"read\",\"write\"]}"}`,
2087+
expectInBody: true,
2088+
expectedValue: `{"FavoriteColor": "Blue", "file:/c/users/foobar/documents/info.txt": "{\"permissions\":[\"read\",\"write\"]}"}`,
2089+
},
2090+
{
2091+
name: "empty attribute",
2092+
attrValue: "",
2093+
expectInBody: false,
2094+
expectedValue: "",
2095+
},
2096+
{
2097+
name: "complex nested structure",
2098+
attrValue: `{"user":"admin","config":"{\"level\":5,\"permissions\":[\"read\",\"write\",\"execute\"]}"}`,
2099+
expectInBody: true,
2100+
expectedValue: `{"user":"admin","config":"{\"level\":5,\"permissions\":[\"read\",\"write\",\"execute\"]}"}`,
2101+
},
2102+
}
2103+
2104+
for _, tt := range tests {
2105+
t.Run(tt.name, func(t *testing.T) {
2106+
cred, err := NewCredFromSecret(fakeSecret)
2107+
if err != nil {
2108+
t.Fatal(err)
2109+
}
2110+
2111+
lmo := "login.microsoftonline.com"
2112+
tenant := "test-tenant"
2113+
accessToken := "test-access-token"
2114+
authority := fmt.Sprintf(authorityFmt, lmo, tenant)
2115+
2116+
mockClient := mock.NewClient()
2117+
mockClient.AppendResponse(mock.WithBody(mock.GetTenantDiscoveryBody(lmo, tenant)))
2118+
mockClient.AppendResponse(
2119+
mock.WithBody(mock.GetAccessTokenBody(accessToken, mock.GetIDToken(tenant, authority), "", "", 3600, 0)),
2120+
mock.WithCallback(func(r *http.Request) {
2121+
// Parse the request body
2122+
if err := r.ParseForm(); err != nil {
2123+
t.Fatal(err)
2124+
}
2125+
2126+
// Check if "attributes" parameter is present in the request body
2127+
if tt.expectInBody {
2128+
if !r.Form.Has("attributes") {
2129+
t.Fatal("Expected 'attributes' parameter in request body, but it was not found")
2130+
}
2131+
2132+
actualValue := r.Form.Get("attributes")
2133+
if actualValue != tt.expectedValue {
2134+
t.Fatalf("Expected attributes value %q, got %q", tt.expectedValue, actualValue)
2135+
}
2136+
} else {
2137+
if r.Form.Has("attributes") {
2138+
t.Fatalf("Did not expect 'attributes' parameter in request body, but found: %q", r.Form.Get("attributes"))
2139+
}
2140+
}
2141+
}),
2142+
)
2143+
2144+
client, err := New(authority, fakeClientID, cred, WithHTTPClient(mockClient), WithInstanceDiscovery(false))
2145+
if err != nil {
2146+
t.Fatal(err)
2147+
}
2148+
2149+
ctx := context.Background()
2150+
var ar AuthResult
2151+
if tt.attrValue != "" {
2152+
ar, err = client.AcquireTokenByCredential(ctx, tokenScope, WithAttribute(tt.attrValue))
2153+
} else {
2154+
ar, err = client.AcquireTokenByCredential(ctx, tokenScope, WithAttribute(tt.attrValue))
2155+
}
2156+
2157+
if err != nil {
2158+
t.Fatalf("AcquireTokenByCredential failed: %v", err)
2159+
}
2160+
2161+
if ar.AccessToken != accessToken {
2162+
t.Fatalf("Expected access token %q, got %q", accessToken, ar.AccessToken)
2163+
}
2164+
})
2165+
}
2166+
}
2167+
2168+
// TestWithAttributeAndFMIPath validates that WithAttribute and WithFMIPath can be used together
2169+
// and both values are correctly passed to extraBodyParameters
2170+
func TestWithAttributeAndFMIPath(t *testing.T) {
2171+
cred, err := NewCredFromSecret(fakeSecret)
2172+
if err != nil {
2173+
t.Fatal(err)
2174+
}
2175+
2176+
lmo := "login.microsoftonline.com"
2177+
tenant := "test-tenant"
2178+
accessToken := "test-access-token"
2179+
authority := fmt.Sprintf(authorityFmt, lmo, tenant)
2180+
fmiPath := "test/fmi/path"
2181+
attrValue := `{"color":"blue","size":"large"}`
2182+
2183+
mockClient := mock.NewClient()
2184+
mockClient.AppendResponse(mock.WithBody(mock.GetTenantDiscoveryBody(lmo, tenant)))
2185+
mockClient.AppendResponse(
2186+
mock.WithBody(mock.GetAccessTokenBody(accessToken, mock.GetIDToken(tenant, authority), "", "", 3600, 0)),
2187+
mock.WithCallback(func(r *http.Request) {
2188+
if err := r.ParseForm(); err != nil {
2189+
t.Fatal(err)
2190+
}
2191+
2192+
// Verify both parameters are present
2193+
if !r.Form.Has("fmi_path") {
2194+
t.Fatal("Expected 'fmi_path' parameter in request body")
2195+
}
2196+
if !r.Form.Has("attributes") {
2197+
t.Fatal("Expected 'attributes' parameter in request body")
2198+
}
2199+
2200+
// Verify values
2201+
if actualFMI := r.Form.Get("fmi_path"); actualFMI != fmiPath {
2202+
t.Fatalf("Expected fmi_path %q, got %q", fmiPath, actualFMI)
2203+
}
2204+
if actualAttr := r.Form.Get("attributes"); actualAttr != attrValue {
2205+
t.Fatalf("Expected attributes %q, got %q", attrValue, actualAttr)
2206+
}
2207+
}),
2208+
)
2209+
2210+
client, err := New(authority, fakeClientID, cred, WithHTTPClient(mockClient), WithInstanceDiscovery(false))
2211+
if err != nil {
2212+
t.Fatal(err)
2213+
}
2214+
2215+
ctx := context.Background()
2216+
ar, err := client.AcquireTokenByCredential(ctx, tokenScope, WithFMIPath(fmiPath), WithAttribute(attrValue))
2217+
if err != nil {
2218+
t.Fatalf("AcquireTokenByCredential failed: %v", err)
2219+
}
2220+
2221+
if ar.AccessToken != accessToken {
2222+
t.Fatalf("Expected access token %q, got %q", accessToken, ar.AccessToken)
2223+
}
2224+
}

apps/internal/oauth/ops/authority/authority_ext_cachekey_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,32 @@ func TestAppKeyWithCacheKeyComponent(t *testing.T) {
136136
},
137137
wantedExtraCacheKey: "3-rg6_wyjx5bcy0c3cqq7gajtzgsqy3oxqpwj4y8k4u",
138138
},
139+
{
140+
name: "with extra 5 params",
141+
clientID: "client1",
142+
tenant: "tenant1",
143+
params: map[string]string{
144+
"key3": "value3",
145+
"key4": "value4",
146+
"key5": "value5",
147+
"key6": "value6",
148+
"key7": "value7",
149+
},
150+
wantedExtraCacheKey: "gkpxxkkqjxcqnvnmr2duvxg66xanvkz6qfqpwp2e",
151+
},
152+
{
153+
name: "with extra 5 params different order ",
154+
clientID: "client1",
155+
tenant: "tenant1",
156+
params: map[string]string{
157+
"key7": "value7",
158+
"key4": "value4",
159+
"key6": "value6",
160+
"key5": "value5",
161+
"key3": "value3",
162+
},
163+
wantedExtraCacheKey: "gkpxxkkqjxcqnvnmr2duvxg66xanvkz6qfqpwp2e",
164+
},
139165
}
140166

141167
for _, tt := range tests {

0 commit comments

Comments
 (0)