Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Set audience with UMATokenSourceOption upon UMATokenSource instantiation #40

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
6 changes: 3 additions & 3 deletions client/client_with_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 28 additions & 3 deletions client/uma_token_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions client/uma_token_source_options.go
Original file line number Diff line number Diff line change
@@ -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
}
}
114 changes: 114 additions & 0 deletions client/uma_token_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package client

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
Expand All @@ -29,6 +31,7 @@ const (
"token_type":"Bearer",
"not-before-policy":0
}`
realm = "testRealm"
)

var (
Expand Down Expand Up @@ -145,3 +148,114 @@ 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) {
// Skip asserting or validation on the 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){
const(
audienceFromTokenOptions = "aud-cof-from-token-method"
audienceFromSourceOptions = "aud-conf-from-options"
testAudience = "test-aud"
)

cases := map[string] struct{
AudienceFromSourceOptions string
AudienceFromTokenOptions string
ExpectedOptionsSent []string
}{
"No audience" : {
AudienceFromTokenOptions: "",
AudienceFromSourceOptions: "",
ExpectedOptionsSent: []string{},
},
"Different audiences are provided from each" : {
AudienceFromTokenOptions: audienceFromTokenOptions,
AudienceFromSourceOptions: audienceFromSourceOptions,
ExpectedOptionsSent: []string{audienceFromSourceOptions, audienceFromTokenOptions},
},
"Only token option provided" : {
AudienceFromTokenOptions: audienceFromTokenOptions,
AudienceFromSourceOptions: "",
ExpectedOptionsSent: []string{audienceFromTokenOptions},
},
"Only source option provided" : {
AudienceFromTokenOptions: "",
AudienceFromSourceOptions: audienceFromSourceOptions,
ExpectedOptionsSent: []string{audienceFromSourceOptions},
},
"Both provides same audience" : {
AudienceFromTokenOptions: testAudience,
AudienceFromSourceOptions: testAudience,
ExpectedOptionsSent: []string{testAudience},
},
}

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)
}
}))
}