From 0ce1cbcf39a323a57be3a65ed6617d9a6a0296e1 Mon Sep 17 00:00:00 2001 From: Andras Acs Date: Fri, 18 Dec 2020 16:53:30 +0100 Subject: [PATCH 1/2] Set audience with UMATokenSourceOption upon UMATokenSource instantiation --- .gitignore | 1 + client/client_with_secret.go | 6 +- client/uma_token_source.go | 31 ++++++++- client/uma_token_source_options.go | 20 ++++++ client/uma_token_source_test.go | 108 +++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 .gitignore create mode 100644 client/uma_token_source_options.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/client/client_with_secret.go b/client/client_with_secret.go index 2b7eda9..ac020f8 100644 --- a/client/client_with_secret.go +++ b/client/client_with_secret.go @@ -17,7 +17,7 @@ type AuthProvider interface { ManagedHTTPClient(...HTTPClientOption) *http.Client HTTPClient(...HTTPClientOption) *http.Client TokenSource() oauth2.TokenSource - UMATokenSource() UMATokenSource + UMATokenSource(options ...UMATokenSourceOption) UMATokenSource } var clients sync.Map @@ -71,8 +71,8 @@ func (cws *WithSecret) TokenSource() oauth2.TokenSource { } // UMATokenSource returns an UMA token source. -func (cws *WithSecret) UMATokenSource() UMATokenSource { - return newUMATokenSource(cws.credentials) +func (cws *WithSecret) UMATokenSource(options ...UMATokenSourceOption) UMATokenSource { + return newUMATokenSource(cws.credentials, options...) } // ManagedHTTPClient is a preconfigured http client using in-memory client storage diff --git a/client/uma_token_source.go b/client/uma_token_source.go index 9bb7ec1..7eaaa4e 100644 --- a/client/uma_token_source.go +++ b/client/uma_token_source.go @@ -53,23 +53,34 @@ type UMATokenSource interface { } type umaTokenSource struct { - config clientcredentials.Config + config clientcredentials.Config + options []UMATokenSourceOption } // NewUMATokenSource returns a new UMA token source. -func newUMATokenSource(config clientcredentials.Config) umaTokenSource { +func newUMATokenSource(config clientcredentials.Config, options ...UMATokenSourceOption) umaTokenSource { return umaTokenSource{ - config: config, + config: config, + options: options, } } // Token returns a new UMA token upon each invocation. func (tokenSource umaTokenSource) Token(claim interface{}, permisson []Permission, audienceConfig config.AudienceConfig) (*oauth2.Token, error) { + tokenSourceConfig := &UMATokenSourceConfig{} + for _, opt := range tokenSource.options { + opt(tokenSourceConfig) + } + encodedClaim, err := encodeClaim(claim) if err != nil { return nil, err } + if tokenSourceConfig.audience != nil { + audienceConfig = mergeAudienceConfigs(*tokenSourceConfig.audience, audienceConfig) + } + request, err := tokenSource.newTokenRequest(encodedClaim, permisson, audienceConfig) if err != nil { return nil, err @@ -93,6 +104,20 @@ func (tokenSource umaTokenSource) Token(claim interface{}, permisson []Permissio return token, nil } +func mergeAudienceConfigs(a, b config.AudienceConfig) config.AudienceConfig { + if len(b.All()) == 0 { + b = a + } else { + allAudiences := append(b.All(), a.All()...) + if len(allAudiences) > 1 { + b = config.NewAudienceConfig(allAudiences[0], allAudiences[1:]...) + } else { + b = config.NewAudienceConfig(allAudiences[0]) + } + } + return b +} + func encodeClaim(claim interface{}) (string, error) { bytes, err := json.Marshal(claim) if err != nil { diff --git a/client/uma_token_source_options.go b/client/uma_token_source_options.go new file mode 100644 index 0000000..732fb90 --- /dev/null +++ b/client/uma_token_source_options.go @@ -0,0 +1,20 @@ +package client + +import "github.com/bitrise-io/bitrise-oauth/config" + +// UMATokenSourceOption represents a configuration option +type UMATokenSourceOption func(u *UMATokenSourceConfig) + +// UMATokenSourceConfig represents the configuration of an +// UMATokenSource. +type UMATokenSourceConfig struct { + audience *config.AudienceConfig +} + +// WithAudienceConfig returns a function, which sets the audience +// to the provided audience configuration. +func WithAudienceConfig(c config.AudienceConfig) UMATokenSourceOption { + return func(u *UMATokenSourceConfig) { + u.audience = &c + } +} diff --git a/client/uma_token_source_test.go b/client/uma_token_source_test.go index a5db855..9b15c78 100644 --- a/client/uma_token_source_test.go +++ b/client/uma_token_source_test.go @@ -2,9 +2,11 @@ package client import ( "bytes" + "encoding/json" "fmt" "io/ioutil" "net/http" + "net/http/httptest" "net/url" "testing" "time" @@ -29,6 +31,7 @@ const ( "token_type":"Bearer", "not-before-policy":0 }` + realm = "testRealm" ) var ( @@ -145,3 +148,108 @@ func Test_GivenSuccessfulTokenResponse_WhenTokenIsExtractedFromBody_ThenExpectTo func urlEncodedBodyParam(key, value string) string { return fmt.Sprintf("%s=%s", url.QueryEscape(key), url.QueryEscape(value)) } + +func Test_GivenUMATokenSource_WhenTokenAsked_NoErrors(t *testing.T){ + ts := newAssertingMockServer(t, realm, func(t *testing.T, r *http.Request) { + + }) + defer ts.Close() + authProvider := NewWithSecret( + "test-client-id", + "test-client-secret", + WithScope("test"), + WithBaseURL(ts.URL), + WithRealm(realm)) + tokenSource := authProvider.UMATokenSource() + _, err := tokenSource.Token(nil, nil, audienceConfig) + require.NoError(t, err) +} + +func Test_GivenAudienceConfiguration_WhenUMATokenSourceIsInstantiated_ThenTokenCallsWithAudience(t *testing.T){ + cases := map[string] struct{ + AudienceFromSourceOptions string + AudienceFromTokenOptions string + ExpectedOptionsSent []string + }{ + "No audience" : { + AudienceFromTokenOptions: "", + AudienceFromSourceOptions: "", + ExpectedOptionsSent: []string{}, + }, + "Different audiences are provided from each" : { + AudienceFromTokenOptions: "aud-cof-from-token-method", + AudienceFromSourceOptions: "aud-conf-from-options", + ExpectedOptionsSent: []string{"aud-cof-from-token-method", "aud-conf-from-options"}, + }, + "Only token option provided" : { + AudienceFromTokenOptions: "aud-cof-from-token-method", + AudienceFromSourceOptions: "", + ExpectedOptionsSent: []string{"aud-cof-from-token-method"}, + }, + "Only source option provided" : { + AudienceFromTokenOptions: "", + AudienceFromSourceOptions: "aud-conf-from-options", + ExpectedOptionsSent: []string{"aud-conf-from-options"}, + }, + "Both provides same audience" : { + AudienceFromTokenOptions: "test-aud", + AudienceFromSourceOptions: "test-aud", + ExpectedOptionsSent: []string{"test-aud"}, + }, + } + + for _, c := range cases { + runAudiencesTestCase( + t, + c.AudienceFromTokenOptions, + c.AudienceFromSourceOptions, + c.ExpectedOptionsSent) + } +} + +func runAudiencesTestCase( + t *testing.T, + audFromToken string, + audFromOptions string, + expectedOptionsSet []string ) { + ts := newAssertingMockServer(t, realm, func(t *testing.T, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + body := string(b) + for _, expected := range expectedOptionsSet { + assert.Contains(t, body, urlEncodedBodyParam(audience, expected)) + } + }) + defer ts.Close() + + authProvider := NewWithSecret( + "test-client-id", + "test-client-secret", + WithScope("test"), + WithBaseURL(ts.URL), + WithRealm(realm)) + audConfOpt := WithAudienceConfig(config.NewAudienceConfig(audFromOptions)) + tokenSource := authProvider.UMATokenSource(audConfOpt) + _ ,err := tokenSource.Token(nil, nil, config.NewAudienceConfig(audFromToken)) + require.NoError(t, err) +} + +func newAssertingMockServer( + t *testing.T, + realm string, + assertFunc func(t *testing.T, r *http.Request)) *httptest.Server{ + tokenEndpointURL := "/auth/realms/" + realm +"/protocol/openid-connect/token" + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case tokenEndpointURL: + assertFunc(t, r) + json.NewEncoder(w).Encode(tokenJSON{ + AccessToken: "my-test-token", + RefreshToken: "refresh-token", + TokenType: "Bearer", + ExpiresIn: 3600, + }) + w.WriteHeader(http.StatusOK) + } + })) +} \ No newline at end of file From 4da1abdc0661b79f0e916709e5a8da1fd74576f0 Mon Sep 17 00:00:00 2001 From: Andras Acs Date: Fri, 18 Dec 2020 17:09:15 +0100 Subject: [PATCH 2/2] sonar errors fix --- client/uma_token_source_test.go | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/client/uma_token_source_test.go b/client/uma_token_source_test.go index 9b15c78..57b9050 100644 --- a/client/uma_token_source_test.go +++ b/client/uma_token_source_test.go @@ -151,7 +151,7 @@ func urlEncodedBodyParam(key, value string) string { func Test_GivenUMATokenSource_WhenTokenAsked_NoErrors(t *testing.T){ ts := newAssertingMockServer(t, realm, func(t *testing.T, r *http.Request) { - + // Skip asserting or validation on the request. }) defer ts.Close() authProvider := NewWithSecret( @@ -166,6 +166,12 @@ func Test_GivenUMATokenSource_WhenTokenAsked_NoErrors(t *testing.T){ } func Test_GivenAudienceConfiguration_WhenUMATokenSourceIsInstantiated_ThenTokenCallsWithAudience(t *testing.T){ + const( + audienceFromTokenOptions = "aud-cof-from-token-method" + audienceFromSourceOptions = "aud-conf-from-options" + testAudience = "test-aud" + ) + cases := map[string] struct{ AudienceFromSourceOptions string AudienceFromTokenOptions string @@ -177,24 +183,24 @@ func Test_GivenAudienceConfiguration_WhenUMATokenSourceIsInstantiated_ThenTokenC ExpectedOptionsSent: []string{}, }, "Different audiences are provided from each" : { - AudienceFromTokenOptions: "aud-cof-from-token-method", - AudienceFromSourceOptions: "aud-conf-from-options", - ExpectedOptionsSent: []string{"aud-cof-from-token-method", "aud-conf-from-options"}, + AudienceFromTokenOptions: audienceFromTokenOptions, + AudienceFromSourceOptions: audienceFromSourceOptions, + ExpectedOptionsSent: []string{audienceFromSourceOptions, audienceFromTokenOptions}, }, "Only token option provided" : { - AudienceFromTokenOptions: "aud-cof-from-token-method", + AudienceFromTokenOptions: audienceFromTokenOptions, AudienceFromSourceOptions: "", - ExpectedOptionsSent: []string{"aud-cof-from-token-method"}, + ExpectedOptionsSent: []string{audienceFromTokenOptions}, }, "Only source option provided" : { AudienceFromTokenOptions: "", - AudienceFromSourceOptions: "aud-conf-from-options", - ExpectedOptionsSent: []string{"aud-conf-from-options"}, + AudienceFromSourceOptions: audienceFromSourceOptions, + ExpectedOptionsSent: []string{audienceFromSourceOptions}, }, "Both provides same audience" : { - AudienceFromTokenOptions: "test-aud", - AudienceFromSourceOptions: "test-aud", - ExpectedOptionsSent: []string{"test-aud"}, + AudienceFromTokenOptions: testAudience, + AudienceFromSourceOptions: testAudience, + ExpectedOptionsSent: []string{testAudience}, }, }