From b1b6382a72167d433c14114b5d84abf7a4f35529 Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Mon, 13 May 2024 17:40:04 +0000 Subject: [PATCH 1/4] Support federated credentials in on-behalf-of flow --- sdk/azidentity/on_behalf_of_credential.go | 10 +++++ .../on_behalf_of_credential_test.go | 44 +++++++++++++++---- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/sdk/azidentity/on_behalf_of_credential.go b/sdk/azidentity/on_behalf_of_credential.go index 9fb3e2f5c3e2..297d436dd16c 100644 --- a/sdk/azidentity/on_behalf_of_credential.go +++ b/sdk/azidentity/on_behalf_of_credential.go @@ -60,6 +60,16 @@ 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) { + 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) diff --git a/sdk/azidentity/on_behalf_of_credential_test.go b/sdk/azidentity/on_behalf_of_credential_test.go index 1dd39c900dc0..2c7bedde0d74 100644 --- a/sdk/azidentity/on_behalf_of_credential_test.go +++ b/sdk/azidentity/on_behalf_of_credential_test.go @@ -15,37 +15,62 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/stretchr/testify/require" ) func TestOnBehalfOfCredential(t *testing.T) { - expectedAssertion := "user-assertion" + clientAssertion := "client-assertion" + userAssertion := "user-assertion" certs, key := allCertTests[0].certs, allCertTests[0].key for _, test := range []struct { - ctor func(policy.Transporter) (*OnBehalfOfCredential, error) - name string - sendX5C bool + ctor func(policy.Transporter) (*OnBehalfOfCredential, error) + name string + sendX5C bool + verifyCredential func(*testing.T, *http.Request) }{ { ctor: func(tp policy.Transporter) (*OnBehalfOfCredential, error) { o := OnBehalfOfCredentialOptions{ClientOptions: policy.ClientOptions{Transport: tp}} - return NewOnBehalfOfCredentialWithCertificate(fakeTenantID, fakeClientID, expectedAssertion, certs, key, &o) + getAssertions := func(context.Context) (string, error) { + return clientAssertion, nil + } + return NewOnBehalfOfCredentialWithClientAssertions(fakeTenantID, fakeClientID, userAssertion, getAssertions, &o) + }, + name: "client assertions", + verifyCredential: func(t *testing.T, r *http.Request) { + require.Equal(t, clientAssertion, r.FormValue("client_assertion")) + }, + }, + { + ctor: func(tp policy.Transporter) (*OnBehalfOfCredential, error) { + o := OnBehalfOfCredentialOptions{ClientOptions: policy.ClientOptions{Transport: tp}} + return NewOnBehalfOfCredentialWithCertificate(fakeTenantID, fakeClientID, userAssertion, certs, key, &o) }, name: "certificate", + verifyCredential: func(t *testing.T, r *http.Request) { + require.NotEmpty(t, r.FormValue("client_assertion")) + }, }, { ctor: func(tp policy.Transporter) (*OnBehalfOfCredential, error) { o := OnBehalfOfCredentialOptions{ClientOptions: policy.ClientOptions{Transport: tp}, SendCertificateChain: true} - return NewOnBehalfOfCredentialWithCertificate(fakeTenantID, fakeClientID, expectedAssertion, certs, key, &o) + return NewOnBehalfOfCredentialWithCertificate(fakeTenantID, fakeClientID, userAssertion, certs, key, &o) }, name: "SNI", sendX5C: true, + verifyCredential: func(t *testing.T, r *http.Request) { + require.NotEmpty(t, r.FormValue("client_assertion")) + }, }, { ctor: func(tp policy.Transporter) (*OnBehalfOfCredential, error) { o := OnBehalfOfCredentialOptions{ClientOptions: policy.ClientOptions{Transport: tp}} - return NewOnBehalfOfCredentialWithSecret(fakeTenantID, fakeClientID, expectedAssertion, "secret", &o) + return NewOnBehalfOfCredentialWithSecret(fakeTenantID, fakeClientID, userAssertion, fakeSecret, &o) }, name: "secret", + verifyCredential: func(t *testing.T, r *http.Request) { + require.Equal(t, fakeSecret, r.FormValue("client_secret")) + }, }, } { t.Run(test.name, func(t *testing.T) { @@ -63,12 +88,13 @@ func TestOnBehalfOfCredential(t *testing.T) { if scope := r.FormValue("scope"); !strings.Contains(scope, liveTestScope) { t.Errorf(`unexpected scopes "%v"`, scope) } - if assertion := r.FormValue("assertion"); assertion != expectedAssertion { - t.Errorf(`unexpected assertion "%s"`, assertion) + if assertion := r.FormValue("assertion"); assertion != userAssertion { + t.Errorf(`unexpected user assertion "%s"`, assertion) } if test.sendX5C { validateX5C(t, certs)(r) } + test.verifyCredential(t, r) return nil }} cred, err := test.ctor(&srv) From e39ebc878d526ff3e26d71d1044cd9e31157839c Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Wed, 15 May 2024 17:31:29 +0000 Subject: [PATCH 2/4] correct example --- sdk/azidentity/example_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/azidentity/example_test.go b/sdk/azidentity/example_test.go index 878876928b6e..2d9faccc2e29 100644 --- a/sdk/azidentity/example_test.go +++ b/sdk/azidentity/example_test.go @@ -64,7 +64,9 @@ func ExampleNewOnBehalfOfCredentialWithCertificate() { // TODO: handle error } - cred, err = azidentity.NewClientCertificateCredential(tenantID, clientID, certs, key, nil) + // userAssertion is the user's access token for the application. Typically it comes from a client request. + userAssertion := "TODO" + cred, err = azidentity.NewOnBehalfOfCredentialWithCertificate(tenantID, clientID, userAssertion, certs, key, nil) if err != nil { // TODO: handle error } From 7cce7bdcbad9d7957cdd23936e7fc0af22c75d2f Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Wed, 15 May 2024 18:15:49 +0000 Subject: [PATCH 3/4] changelog --- sdk/azidentity/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/azidentity/CHANGELOG.md b/sdk/azidentity/CHANGELOG.md index 79ad129d4ba6..3eba170b1103 100644 --- a/sdk/azidentity/CHANGELOG.md +++ b/sdk/azidentity/CHANGELOG.md @@ -3,6 +3,8 @@ ## 1.6.0-beta.5 (Unreleased) ### Features Added +* `NewOnBehalfOfCredentialWithClientAssertions` creates an on-behalf-of credential + that authenticates with client assertions such as federated credentials ### Breaking Changes From ccc2fd3a9c64b594e9e77303fe5f8152a335244f Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Wed, 15 May 2024 20:59:50 +0000 Subject: [PATCH 4/4] nil getAssertion is a constructor error --- sdk/azidentity/on_behalf_of_credential.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/azidentity/on_behalf_of_credential.go b/sdk/azidentity/on_behalf_of_credential.go index 297d436dd16c..9dcc82f013ba 100644 --- a/sdk/azidentity/on_behalf_of_credential.go +++ b/sdk/azidentity/on_behalf_of_credential.go @@ -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" @@ -64,6 +65,9 @@ func NewOnBehalfOfCredentialWithCertificate(tenantID, clientID, userAssertion st // 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) })