From b8f0ef8fe71893a8ea84e0334f6a75c346f8167a Mon Sep 17 00:00:00 2001 From: sispeo <42068883+fperot74@users.noreply.github.com> Date: Fri, 11 Oct 2019 12:34:33 +0200 Subject: [PATCH] [CLOUDTRUST-1801] Multi issuers support --- Gopkg.lock | 32 +++++++++------- Gopkg.toml | 2 +- integration/integration.go | 3 +- issuer.go | 75 ++++++++++++++++++++++++++++++++++++++ issuer_test.go | 46 +++++++++++++++++++++++ keycloak_client.go | 36 ++++++++---------- oidc_verifier.go | 20 +++++----- 7 files changed, 167 insertions(+), 47 deletions(-) create mode 100644 issuer.go create mode 100644 issuer_test.go diff --git a/Gopkg.lock b/Gopkg.lock index d5f2da7..6599a74 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,12 +2,15 @@ [[projects]] - digest = "1:ba07d490a8733429c256c8a8c78bceec68d5af77ea95ae9ca05d86f833d99644" + digest = "1:0a2237121db84f64bbcbc96bee4b268dceb0badf817824903502b7ab448dbe44" name = "github.com/cloudtrust/common-service" - packages = ["errors"] + packages = [ + ".", + "errors", + ] pruneopts = "" - revision = "bb18af383d1e95e6ced6966fb39f04594959df69" - version = "v1.0-rc7" + revision = "926680e453c4688a250df9cf4b663ba373787c1b" + version = "v1.1.0" [[projects]] digest = "1:379d34d9efc755fab444199f007819fe99718640f9ccfbdd3f0430340bb02b07" @@ -123,7 +126,7 @@ [[projects]] branch = "master" - digest = "1:4a4c4edf69b61bf98e98d22696aa80c4059384895920de4d5e2fe696068d5f13" + digest = "1:9026a485cbd92c93566178e917d1c049b94b592ad98a6188557f941f5d9360e7" name = "golang.org/x/crypto" packages = [ "ed25519", @@ -131,11 +134,11 @@ "pbkdf2", ] pruneopts = "" - revision = "227b76d455e791cb042b03e633e2f7fbcfdf74a5" + revision = "af544f31c8ac5794d2134b792e9eb714d9d8f9ce" [[projects]] branch = "master" - digest = "1:ce26d94b8841936fff59bb524f4b96ac434f411b780b3aa784da90ee96ae2367" + digest = "1:f18ed86e2dc96177f15764afe4a31c900cf85c2a7b50a57ce1320836223c64f0" name = "golang.org/x/net" packages = [ "context", @@ -144,7 +147,7 @@ "publicsuffix", ] pruneopts = "" - revision = "a8b05e9114ab0cb08faec337c959aed24b68bf50" + revision = "d66e71096ffb9f08f36d9aefcae80ce319de6d68" [[projects]] branch = "master" @@ -183,7 +186,7 @@ version = "v0.3.2" [[projects]] - digest = "1:0568e577f790e9bd0420521cff50580f9b38165a38f217ce68f55c4bbaa97066" + digest = "1:c4404231035fad619a12f82ae3f0f8f9edc1cc7f34e7edad7a28ccac5336cc96" name = "google.golang.org/appengine" packages = [ "internal", @@ -195,8 +198,8 @@ "urlfetch", ] pruneopts = "" - revision = "5f2a59506353b8d5ba8cbbcd9f3c1f41f1eaf079" - version = "v1.6.2" + revision = "971852bfffca25b069c31162ae8f247a3dba083b" + version = "v1.6.5" [[projects]] digest = "1:e3250d192192f02fbb143d50de437cbe967d6be7bd9fad671600942a33269d08" @@ -234,17 +237,18 @@ version = "v2.3.1" [[projects]] - digest = "1:cedccf16b71e86db87a24f8d4c70b0a855872eb967cb906a66b95de56aefbd0d" + digest = "1:ab9547706f32a7535bb4f25d6b58ad00436630593cd3e3ed4602f1613ed84783" name = "gopkg.in/yaml.v2" packages = ["."] pruneopts = "" - revision = "51d6538a90f86fe93ac480b35f37b2be17fef232" - version = "v2.2.2" + revision = "f221b8435cfb71e54062f6c6e99e9ade30b124d5" + version = "v2.2.4" [solve-meta] analyzer-name = "dep" analyzer-version = 1 input-imports = [ + "github.com/cloudtrust/common-service", "github.com/cloudtrust/common-service/errors", "github.com/coreos/go-oidc", "github.com/gbrlsnchs/jwt", diff --git a/Gopkg.toml b/Gopkg.toml index 9d7f447..e9cc8c3 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -22,7 +22,7 @@ [[constraint]] name = "github.com/cloudtrust/common-service" - version = "v1.0-rc7" + version = "v1.1.0" [[constraint]] name = "github.com/pkg/errors" diff --git a/integration/integration.go b/integration/integration.go index b6794da..0b4cfcb 100644 --- a/integration/integration.go +++ b/integration/integration.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "log" "strings" @@ -29,7 +30,7 @@ func main() { log.Fatalf("could not get access token: %v", err) } - err = client.VerifyToken("master", accessToken) + err = client.VerifyToken(context.Background(), "master", accessToken) if err != nil { log.Fatalf("could not validate access token: %v", err) } diff --git a/issuer.go b/issuer.go new file mode 100644 index 0000000..16281d6 --- /dev/null +++ b/issuer.go @@ -0,0 +1,75 @@ +package keycloak + +import ( + "context" + "net/url" + "regexp" + "strings" + "time" + + cs "github.com/cloudtrust/common-service" +) + +// IssuerManager provides URL according to a given context +type IssuerManager interface { + GetIssuer(ctx context.Context) OidcVerifierProvider +} + +type issuerManager struct { + domainToIssuer map[string]OidcVerifierProvider + defaultIssuer OidcVerifierProvider +} + +func getProtocolAndDomain(URL string) string { + var r = regexp.MustCompile(`^\w+:\/\/[^\/]+`) + var match = r.FindStringSubmatch(URL) + if match != nil { + return strings.ToLower(match[0]) + } + // Best effort: if not found return the whole input string + return URL +} + +// NewIssuerManager creates a new URLProvider +func NewIssuerManager(config Config) (IssuerManager, error) { + URLs := config.AddrTokenProvider + // Use default values when clients are not initializing these values + cacheTTL := config.CacheTTL + if cacheTTL == 0 { + cacheTTL = 15 * time.Minute + } + errTolerance := config.ErrorTolerance + if errTolerance == 0 { + errTolerance = time.Minute + } + + var domainToIssuer = make(map[string]OidcVerifierProvider) + var defaultIssuer OidcVerifierProvider + + for _, value := range strings.Split(URLs, " ") { + uToken, err := url.Parse(value) + if err != nil { + return nil, err + } + issuer := NewVerifierCache(uToken, cacheTTL, errTolerance) + domainToIssuer[getProtocolAndDomain(value)] = issuer + if domainToIssuer == nil { + defaultIssuer = issuer + } + } + return &issuerManager{ + domainToIssuer: domainToIssuer, + defaultIssuer: defaultIssuer, + }, nil +} + +func (im *issuerManager) GetIssuer(ctx context.Context) OidcVerifierProvider { + if rawValue := ctx.Value(cs.CtContextIssuerDomain); rawValue != nil { + // The issuer domain has been found in the context + issuerDomain := getProtocolAndDomain(rawValue.(string)) + if issuer, ok := im.domainToIssuer[issuerDomain]; ok { + return issuer + } + } + return im.defaultIssuer +} diff --git a/issuer_test.go b/issuer_test.go new file mode 100644 index 0000000..fc2099d --- /dev/null +++ b/issuer_test.go @@ -0,0 +1,46 @@ +package keycloak + +import ( + "context" + "fmt" + "testing" + + cs "github.com/cloudtrust/common-service" + "github.com/stretchr/testify/assert" +) + +func TestGetProtocolAndDomain(t *testing.T) { + var invalidURL = "not a valid URL" + assert.Equal(t, invalidURL, getProtocolAndDomain(invalidURL)) + assert.Equal(t, "https://elca.ch", getProtocolAndDomain("https://ELCA.CH/PATH/TO/TARGET")) +} + +func TestNewIssuerManager(t *testing.T) { + { + _, err := NewIssuerManager(Config{AddrTokenProvider: ":"}) + assert.NotNil(t, err) + } + + defaultPath := "http://default.domain.com:5555" + myDomainPath := "http://my.domain.com/path/to/somewhere" + otherDomainPath := "http://other.domain.com:2120/" + allDomains := fmt.Sprintf("%s %s %s", defaultPath, myDomainPath, otherDomainPath) + + prov, err := NewIssuerManager(Config{AddrTokenProvider: allDomains}) + assert.Nil(t, err) + assert.NotNil(t, prov) + + // No issuer provided with context + issuerNoContext := prov.GetIssuer(context.Background()) + // Unrecognized issuer provided in context + issuerDefault := prov.GetIssuer(context.WithValue(context.Background(), cs.CtContextIssuerDomain, "http://unknown.issuer.com/one/path")) + // Case insensitive + issuerMyDomain := prov.GetIssuer(context.WithValue(context.Background(), cs.CtContextIssuerDomain, "http://MY.DOMAIN.COM/issuer")) + // Other domain + issuerOtherDomain := prov.GetIssuer(context.WithValue(context.Background(), cs.CtContextIssuerDomain, "http://other.domain.com:2120/any/thing/here")) + + assert.Equal(t, issuerNoContext, issuerDefault) + assert.NotEqual(t, issuerNoContext, issuerMyDomain) + assert.NotEqual(t, issuerNoContext, issuerOtherDomain) + assert.NotEqual(t, issuerMyDomain, issuerOtherDomain) +} diff --git a/keycloak_client.go b/keycloak_client.go index b1fd4d2..e7d5859 100644 --- a/keycloak_client.go +++ b/keycloak_client.go @@ -1,6 +1,7 @@ package keycloak import ( + "context" "encoding/json" "strconv" @@ -30,12 +31,13 @@ type Config struct { // Client is the keycloak client. type Client struct { - apiURL *url.URL - httpClient *gentleman.Client - account *AccountClient - verifierProvider OidcVerifierProvider + apiURL *url.URL + httpClient *gentleman.Client + account *AccountClient + issuerManager IssuerManager } +// AccountClient structure type AccountClient struct { client *Client } @@ -52,10 +54,10 @@ func (e HTTPError) Error() string { // New returns a keycloak client. func New(config Config) (*Client, error) { - var uToken *url.URL + var issuerMgr IssuerManager { var err error - uToken, err = url.Parse(config.AddrTokenProvider) + issuerMgr, err = NewIssuerManager(config) if err != nil { return nil, errors.Wrap(err, MsgErrCannotParse+"."+TokenProviderURL) } @@ -70,16 +72,6 @@ func New(config Config) (*Client, error) { } } - // Use default values when clients are not initializing these values - cacheTTL := config.CacheTTL - if cacheTTL == 0 { - cacheTTL = 15 * time.Minute - } - errTolerance := config.ErrorTolerance - if errTolerance == 0 { - errTolerance = time.Minute - } - var httpClient = gentleman.New() { httpClient = httpClient.URL(uAPI.String()) @@ -87,9 +79,9 @@ func New(config Config) (*Client, error) { } var client = &Client{ - apiURL: uAPI, - httpClient: httpClient, - verifierProvider: NewVerifierCache(uToken, cacheTTL, errTolerance), + apiURL: uAPI, + httpClient: httpClient, + issuerManager: issuerMgr, } client.account = &AccountClient{ @@ -146,14 +138,16 @@ func (c *Client) GetToken(realm string, username string, password string) (strin } // VerifyToken verifies a token. It returns an error it is malformed, expired,... -func (c *Client) VerifyToken(realmName string, accessToken string) error { - verifier, err := c.verifierProvider.GetOidcVerifier(realmName) +func (c *Client) VerifyToken(ctx context.Context, realmName string, accessToken string) error { + issuer := c.issuerManager.GetIssuer(ctx) + verifier, err := issuer.GetOidcVerifier(realmName) if err != nil { return err } return verifier.Verify(accessToken) } +// AccountClient gets the associated AccountClient func (c *Client) AccountClient() *AccountClient { return c.account } diff --git a/oidc_verifier.go b/oidc_verifier.go index 71507c1..35f1b39 100644 --- a/oidc_verifier.go +++ b/oidc_verifier.go @@ -21,10 +21,10 @@ type OidcVerifier interface { } type verifierCache struct { - duration time.Duration - errorTolerance time.Duration - tokenProviderURL *url.URL - verifiers map[string]cachedVerifier + duration time.Duration + errorTolerance time.Duration + tokenURL *url.URL + verifiers map[string]cachedVerifier } type cachedVerifier struct { @@ -35,12 +35,12 @@ type cachedVerifier struct { } // NewVerifierCache create an instance of OIDC verifier cache -func NewVerifierCache(tokenProviderURL *url.URL, timeToLive time.Duration, errorTolerance time.Duration) OidcVerifierProvider { +func NewVerifierCache(tokenURL *url.URL, timeToLive time.Duration, errorTolerance time.Duration) OidcVerifierProvider { return &verifierCache{ - duration: timeToLive, - errorTolerance: errorTolerance, - tokenProviderURL: tokenProviderURL, - verifiers: make(map[string]cachedVerifier), + duration: timeToLive, + errorTolerance: errorTolerance, + tokenURL: tokenURL, + verifiers: make(map[string]cachedVerifier), } } @@ -52,7 +52,7 @@ func (vc *verifierCache) GetOidcVerifier(realm string) (OidcVerifier, error) { var oidcProvider *oidc.Provider { var err error - var issuer = fmt.Sprintf("%s/auth/realms/%s", vc.tokenProviderURL.String(), realm) + var issuer = fmt.Sprintf("%s/auth/realms/%s", vc.tokenURL.String(), realm) oidcProvider, err = oidc.NewProvider(context.Background(), issuer) if err != nil { return nil, errors.Wrap(err, MsgErrCannotCreate+"."+OIDCProvider)