Browse Source

[CLOUDTRUST-1801] Multi issuers support

master
sispeo 6 years ago
committed by harture
parent
commit
b8f0ef8fe7
  1. 32
      Gopkg.lock
  2. 2
      Gopkg.toml
  3. 3
      integration/integration.go
  4. 75
      issuer.go
  5. 46
      issuer_test.go
  6. 36
      keycloak_client.go
  7. 20
      oidc_verifier.go

32
Gopkg.lock

@ -2,12 +2,15 @@
[[projects]] [[projects]]
digest = "1:ba07d490a8733429c256c8a8c78bceec68d5af77ea95ae9ca05d86f833d99644" digest = "1:0a2237121db84f64bbcbc96bee4b268dceb0badf817824903502b7ab448dbe44"
name = "github.com/cloudtrust/common-service" name = "github.com/cloudtrust/common-service"
packages = ["errors"] packages = [
".",
"errors",
]
pruneopts = "" pruneopts = ""
revision = "bb18af383d1e95e6ced6966fb39f04594959df69" revision = "926680e453c4688a250df9cf4b663ba373787c1b"
version = "v1.0-rc7" version = "v1.1.0"
[[projects]] [[projects]]
digest = "1:379d34d9efc755fab444199f007819fe99718640f9ccfbdd3f0430340bb02b07" digest = "1:379d34d9efc755fab444199f007819fe99718640f9ccfbdd3f0430340bb02b07"
@ -123,7 +126,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:4a4c4edf69b61bf98e98d22696aa80c4059384895920de4d5e2fe696068d5f13" digest = "1:9026a485cbd92c93566178e917d1c049b94b592ad98a6188557f941f5d9360e7"
name = "golang.org/x/crypto" name = "golang.org/x/crypto"
packages = [ packages = [
"ed25519", "ed25519",
@ -131,11 +134,11 @@
"pbkdf2", "pbkdf2",
] ]
pruneopts = "" pruneopts = ""
revision = "227b76d455e791cb042b03e633e2f7fbcfdf74a5" revision = "af544f31c8ac5794d2134b792e9eb714d9d8f9ce"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:ce26d94b8841936fff59bb524f4b96ac434f411b780b3aa784da90ee96ae2367" digest = "1:f18ed86e2dc96177f15764afe4a31c900cf85c2a7b50a57ce1320836223c64f0"
name = "golang.org/x/net" name = "golang.org/x/net"
packages = [ packages = [
"context", "context",
@ -144,7 +147,7 @@
"publicsuffix", "publicsuffix",
] ]
pruneopts = "" pruneopts = ""
revision = "a8b05e9114ab0cb08faec337c959aed24b68bf50" revision = "d66e71096ffb9f08f36d9aefcae80ce319de6d68"
[[projects]] [[projects]]
branch = "master" branch = "master"
@ -183,7 +186,7 @@
version = "v0.3.2" version = "v0.3.2"
[[projects]] [[projects]]
digest = "1:0568e577f790e9bd0420521cff50580f9b38165a38f217ce68f55c4bbaa97066" digest = "1:c4404231035fad619a12f82ae3f0f8f9edc1cc7f34e7edad7a28ccac5336cc96"
name = "google.golang.org/appengine" name = "google.golang.org/appengine"
packages = [ packages = [
"internal", "internal",
@ -195,8 +198,8 @@
"urlfetch", "urlfetch",
] ]
pruneopts = "" pruneopts = ""
revision = "5f2a59506353b8d5ba8cbbcd9f3c1f41f1eaf079" revision = "971852bfffca25b069c31162ae8f247a3dba083b"
version = "v1.6.2" version = "v1.6.5"
[[projects]] [[projects]]
digest = "1:e3250d192192f02fbb143d50de437cbe967d6be7bd9fad671600942a33269d08" digest = "1:e3250d192192f02fbb143d50de437cbe967d6be7bd9fad671600942a33269d08"
@ -234,17 +237,18 @@
version = "v2.3.1" version = "v2.3.1"
[[projects]] [[projects]]
digest = "1:cedccf16b71e86db87a24f8d4c70b0a855872eb967cb906a66b95de56aefbd0d" digest = "1:ab9547706f32a7535bb4f25d6b58ad00436630593cd3e3ed4602f1613ed84783"
name = "gopkg.in/yaml.v2" name = "gopkg.in/yaml.v2"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232" revision = "f221b8435cfb71e54062f6c6e99e9ade30b124d5"
version = "v2.2.2" version = "v2.2.4"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
input-imports = [ input-imports = [
"github.com/cloudtrust/common-service",
"github.com/cloudtrust/common-service/errors", "github.com/cloudtrust/common-service/errors",
"github.com/coreos/go-oidc", "github.com/coreos/go-oidc",
"github.com/gbrlsnchs/jwt", "github.com/gbrlsnchs/jwt",

2
Gopkg.toml

@ -22,7 +22,7 @@
[[constraint]] [[constraint]]
name = "github.com/cloudtrust/common-service" name = "github.com/cloudtrust/common-service"
version = "v1.0-rc7" version = "v1.1.0"
[[constraint]] [[constraint]]
name = "github.com/pkg/errors" name = "github.com/pkg/errors"

3
integration/integration.go

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"strings" "strings"
@ -29,7 +30,7 @@ func main() {
log.Fatalf("could not get access token: %v", err) log.Fatalf("could not get access token: %v", err)
} }
err = client.VerifyToken("master", accessToken) err = client.VerifyToken(context.Background(), "master", accessToken)
if err != nil { if err != nil {
log.Fatalf("could not validate access token: %v", err) log.Fatalf("could not validate access token: %v", err)
} }

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

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

36
keycloak_client.go

@ -1,6 +1,7 @@
package keycloak package keycloak
import ( import (
"context"
"encoding/json" "encoding/json"
"strconv" "strconv"
@ -30,12 +31,13 @@ type Config struct {
// Client is the keycloak client. // Client is the keycloak client.
type Client struct { type Client struct {
apiURL *url.URL apiURL *url.URL
httpClient *gentleman.Client httpClient *gentleman.Client
account *AccountClient account *AccountClient
verifierProvider OidcVerifierProvider issuerManager IssuerManager
} }
// AccountClient structure
type AccountClient struct { type AccountClient struct {
client *Client client *Client
} }
@ -52,10 +54,10 @@ func (e HTTPError) Error() string {
// New returns a keycloak client. // New returns a keycloak client.
func New(config Config) (*Client, error) { func New(config Config) (*Client, error) {
var uToken *url.URL var issuerMgr IssuerManager
{ {
var err error var err error
uToken, err = url.Parse(config.AddrTokenProvider) issuerMgr, err = NewIssuerManager(config)
if err != nil { if err != nil {
return nil, errors.Wrap(err, MsgErrCannotParse+"."+TokenProviderURL) 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() var httpClient = gentleman.New()
{ {
httpClient = httpClient.URL(uAPI.String()) httpClient = httpClient.URL(uAPI.String())
@ -87,9 +79,9 @@ func New(config Config) (*Client, error) {
} }
var client = &Client{ var client = &Client{
apiURL: uAPI, apiURL: uAPI,
httpClient: httpClient, httpClient: httpClient,
verifierProvider: NewVerifierCache(uToken, cacheTTL, errTolerance), issuerManager: issuerMgr,
} }
client.account = &AccountClient{ 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,... // VerifyToken verifies a token. It returns an error it is malformed, expired,...
func (c *Client) VerifyToken(realmName string, accessToken string) error { func (c *Client) VerifyToken(ctx context.Context, realmName string, accessToken string) error {
verifier, err := c.verifierProvider.GetOidcVerifier(realmName) issuer := c.issuerManager.GetIssuer(ctx)
verifier, err := issuer.GetOidcVerifier(realmName)
if err != nil { if err != nil {
return err return err
} }
return verifier.Verify(accessToken) return verifier.Verify(accessToken)
} }
// AccountClient gets the associated AccountClient
func (c *Client) AccountClient() *AccountClient { func (c *Client) AccountClient() *AccountClient {
return c.account return c.account
} }

20
oidc_verifier.go

@ -21,10 +21,10 @@ type OidcVerifier interface {
} }
type verifierCache struct { type verifierCache struct {
duration time.Duration duration time.Duration
errorTolerance time.Duration errorTolerance time.Duration
tokenProviderURL *url.URL tokenURL *url.URL
verifiers map[string]cachedVerifier verifiers map[string]cachedVerifier
} }
type cachedVerifier struct { type cachedVerifier struct {
@ -35,12 +35,12 @@ type cachedVerifier struct {
} }
// NewVerifierCache create an instance of OIDC verifier cache // 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{ return &verifierCache{
duration: timeToLive, duration: timeToLive,
errorTolerance: errorTolerance, errorTolerance: errorTolerance,
tokenProviderURL: tokenProviderURL, tokenURL: tokenURL,
verifiers: make(map[string]cachedVerifier), verifiers: make(map[string]cachedVerifier),
} }
} }
@ -52,7 +52,7 @@ func (vc *verifierCache) GetOidcVerifier(realm string) (OidcVerifier, error) {
var oidcProvider *oidc.Provider var oidcProvider *oidc.Provider
{ {
var err error 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) oidcProvider, err = oidc.NewProvider(context.Background(), issuer)
if err != nil { if err != nil {
return nil, errors.Wrap(err, MsgErrCannotCreate+"."+OIDCProvider) return nil, errors.Wrap(err, MsgErrCannotCreate+"."+OIDCProvider)

Loading…
Cancel
Save