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. 26
      keycloak_client.go
  7. 8
      oidc_verifier.go

32
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",

2
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"

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

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

26
keycloak_client.go

@ -1,6 +1,7 @@
package keycloak
import (
"context"
"encoding/json"
"strconv"
@ -33,9 +34,10 @@ type Client struct {
apiURL *url.URL
httpClient *gentleman.Client
account *AccountClient
verifierProvider OidcVerifierProvider
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())
@ -89,7 +81,7 @@ func New(config Config) (*Client, error) {
var client = &Client{
apiURL: uAPI,
httpClient: httpClient,
verifierProvider: NewVerifierCache(uToken, cacheTTL, errTolerance),
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
}

8
oidc_verifier.go

@ -23,7 +23,7 @@ type OidcVerifier interface {
type verifierCache struct {
duration time.Duration
errorTolerance time.Duration
tokenProviderURL *url.URL
tokenURL *url.URL
verifiers map[string]cachedVerifier
}
@ -35,11 +35,11 @@ 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,
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)

Loading…
Cancel
Save