From 3df0ca5bf9e7c78ea9d90bdab147ff7a58ffb70c Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Tue, 1 Mar 2016 11:11:57 -0800 Subject: [PATCH 1/2] Godeps: update coreos/go-oidc to remove net/http/httptest import Updates #21114 --- Godeps/Godeps.json | 10 +- .../github.com/coreos/go-oidc/http/client.go | 46 +- .../github.com/coreos/go-oidc/jose/claims.go | 25 +- .../github.com/coreos/go-oidc/jose/jose.go | 51 ++ .../src/github.com/coreos/go-oidc/jose/jwk.go | 4 + .../src/github.com/coreos/go-oidc/key/key.go | 2 +- .../src/github.com/coreos/go-oidc/key/repo.go | 12 +- .../src/github.com/coreos/go-oidc/key/sync.go | 8 +- .../coreos/go-oidc/oauth2/oauth2.go | 47 +- .../github.com/coreos/go-oidc/oidc/client.go | 574 ++++++++++++++++-- .../coreos/go-oidc/oidc/provider.go | 451 +++++++++++++- .../github.com/coreos/go-oidc/oidc/util.go | 4 +- 12 files changed, 1119 insertions(+), 115 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 2fe700bc16f..cede96a9192 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -271,23 +271,23 @@ }, { "ImportPath": "github.com/coreos/go-oidc/http", - "Rev": "024cdeee09d02fb439eb55bc422e582ac115615b" + "Rev": "d7cb66526fffc811d602b6770581064f4b66b507" }, { "ImportPath": "github.com/coreos/go-oidc/jose", - "Rev": "024cdeee09d02fb439eb55bc422e582ac115615b" + "Rev": "d7cb66526fffc811d602b6770581064f4b66b507" }, { "ImportPath": "github.com/coreos/go-oidc/key", - "Rev": "024cdeee09d02fb439eb55bc422e582ac115615b" + "Rev": "d7cb66526fffc811d602b6770581064f4b66b507" }, { "ImportPath": "github.com/coreos/go-oidc/oauth2", - "Rev": "024cdeee09d02fb439eb55bc422e582ac115615b" + "Rev": "d7cb66526fffc811d602b6770581064f4b66b507" }, { "ImportPath": "github.com/coreos/go-oidc/oidc", - "Rev": "024cdeee09d02fb439eb55bc422e582ac115615b" + "Rev": "d7cb66526fffc811d602b6770581064f4b66b507" }, { "ImportPath": "github.com/coreos/go-semver/semver", diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/http/client.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/http/client.go index 942e2bb18cb..fd079b4950f 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/http/client.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/http/client.go @@ -1,51 +1,7 @@ package http -import ( - "io/ioutil" - "net/http" - "net/http/httptest" -) +import "net/http" type Client interface { Do(*http.Request) (*http.Response, error) } - -type HandlerClient struct { - Handler http.Handler -} - -func (hc *HandlerClient) Do(r *http.Request) (*http.Response, error) { - w := httptest.NewRecorder() - hc.Handler.ServeHTTP(w, r) - - resp := http.Response{ - StatusCode: w.Code, - Header: w.Header(), - Body: ioutil.NopCloser(w.Body), - } - - return &resp, nil -} - -type RequestRecorder struct { - Response *http.Response - Error error - - Request *http.Request -} - -func (rr *RequestRecorder) Do(req *http.Request) (*http.Response, error) { - rr.Request = req - - if rr.Response == nil && rr.Error == nil { - panic("RequestRecorder Response and Error cannot both be nil") - } else if rr.Response != nil && rr.Error != nil { - panic("RequestRecorder Response and Error cannot both be non-nil") - } - - return rr.Response, rr.Error -} - -func (rr *RequestRecorder) RoundTrip(req *http.Request) (*http.Response, error) { - return rr.Do(req) -} diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/claims.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/claims.go index 1695b3ac015..8b48bfd230b 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/claims.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/claims.go @@ -3,6 +3,7 @@ package jose import ( "encoding/json" "fmt" + "math" "time" ) @@ -70,13 +71,33 @@ func (c Claims) Int64Claim(name string) (int64, bool, error) { return v, true, nil } +func (c Claims) Float64Claim(name string) (float64, bool, error) { + cl, ok := c[name] + if !ok { + return 0, false, nil + } + + v, ok := cl.(float64) + if !ok { + vi, ok := cl.(int64) + if !ok { + return 0, false, fmt.Errorf("unable to parse claim as float64: %v", name) + } + v = float64(vi) + } + + return v, true, nil +} + func (c Claims) TimeClaim(name string) (time.Time, bool, error) { - v, ok, err := c.Int64Claim(name) + v, ok, err := c.Float64Claim(name) if !ok || err != nil { return time.Time{}, ok, err } - return time.Unix(v, 0).UTC(), true, nil + s := math.Trunc(v) + ns := (v - s) * math.Pow(10, 9) + return time.Unix(int64(s), int64(ns)).UTC(), true, nil } func decodeClaims(payload []byte) (Claims, error) { diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/jose.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/jose.go index 4f99aeb7684..6209926596c 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/jose.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/jose.go @@ -13,6 +13,57 @@ const ( HeaderKeyID = "kid" ) +const ( + // Encryption Algorithm Header Parameter Values for JWS + // See: https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#page-6 + AlgHS256 = "HS256" + AlgHS384 = "HS384" + AlgHS512 = "HS512" + AlgRS256 = "RS256" + AlgRS384 = "RS384" + AlgRS512 = "RS512" + AlgES256 = "ES256" + AlgES384 = "ES384" + AlgES512 = "ES512" + AlgPS256 = "PS256" + AlgPS384 = "PS384" + AlgPS512 = "PS512" + AlgNone = "none" +) + +const ( + // Algorithm Header Parameter Values for JWE + // See: https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-4.1 + AlgRSA15 = "RSA1_5" + AlgRSAOAEP = "RSA-OAEP" + AlgRSAOAEP256 = "RSA-OAEP-256" + AlgA128KW = "A128KW" + AlgA192KW = "A192KW" + AlgA256KW = "A256KW" + AlgDir = "dir" + AlgECDHES = "ECDH-ES" + AlgECDHESA128KW = "ECDH-ES+A128KW" + AlgECDHESA192KW = "ECDH-ES+A192KW" + AlgECDHESA256KW = "ECDH-ES+A256KW" + AlgA128GCMKW = "A128GCMKW" + AlgA192GCMKW = "A192GCMKW" + AlgA256GCMKW = "A256GCMKW" + AlgPBES2HS256A128KW = "PBES2-HS256+A128KW" + AlgPBES2HS384A192KW = "PBES2-HS384+A192KW" + AlgPBES2HS512A256KW = "PBES2-HS512+A256KW" +) + +const ( + // Encryption Algorithm Header Parameter Values for JWE + // See: https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#page-22 + EncA128CBCHS256 = "A128CBC-HS256" + EncA128CBCHS384 = "A128CBC-HS384" + EncA256CBCHS512 = "A256CBC-HS512" + EncA128GCM = "A128GCM" + EncA192GCM = "A192GCM" + EncA256GCM = "A256GCM" +) + type JOSEHeader map[string]string func (j JOSEHeader) Validate() error { diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/jwk.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/jwk.go index 045f5fdee2c..b7a8e235583 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/jwk.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/jose/jwk.go @@ -70,6 +70,10 @@ func (j *JWK) UnmarshalJSON(data []byte) error { return nil } +type JWKSet struct { + Keys []JWK `json:"keys"` +} + func decodeExponent(e string) (int, error) { decE, err := decodeBase64URLPaddingOptional(e) if err != nil { diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/key/key.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/key/key.go index 3edae468f8b..de6250373d0 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/key/key.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/key/key.go @@ -135,7 +135,7 @@ func (s *PrivateKeySet) Active() *PrivateKey { type GeneratePrivateKeyFunc func() (*PrivateKey, error) func GeneratePrivateKey() (*PrivateKey, error) { - pk, err := rsa.GenerateKey(rand.Reader, 1024) + pk, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, err } diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/key/repo.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/key/repo.go index 1d4ce8d3978..1acdeb3614c 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/key/repo.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/key/repo.go @@ -1,6 +1,9 @@ package key -import "errors" +import ( + "errors" + "sync" +) var ErrorNoKeys = errors.New("no keys found") @@ -22,6 +25,7 @@ func NewPrivateKeySetRepo() PrivateKeySetRepo { } type memPrivateKeySetRepo struct { + mu sync.RWMutex pks PrivateKeySet } @@ -33,11 +37,17 @@ func (r *memPrivateKeySetRepo) Set(ks KeySet) error { return errors.New("nil KeySet") } + r.mu.Lock() + defer r.mu.Unlock() + r.pks = *pks return nil } func (r *memPrivateKeySetRepo) Get() (KeySet, error) { + r.mu.RLock() + defer r.mu.RUnlock() + if r.pks.keys == nil { return nil, ErrorNoKeys } diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/key/sync.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/key/sync.go index 076ee462ebc..e8d5d03d881 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/key/sync.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/key/sync.go @@ -29,7 +29,7 @@ func (s *KeySetSyncer) Run() chan struct{} { var failing bool var next time.Duration for { - exp, err := sync(s.readable, s.writable, s.clock) + exp, err := syncKeySet(s.readable, s.writable, s.clock) if err != nil || exp == 0 { if !failing { failing = true @@ -62,12 +62,12 @@ func (s *KeySetSyncer) Run() chan struct{} { } func Sync(r ReadableKeySetRepo, w WritableKeySetRepo) (time.Duration, error) { - return sync(r, w, clockwork.NewRealClock()) + return syncKeySet(r, w, clockwork.NewRealClock()) } -// sync copies the keyset from r to the KeySet at w and returns the duration in which the KeySet will expire. +// syncKeySet copies the keyset from r to the KeySet at w and returns the duration in which the KeySet will expire. // If keyset has already expired, returns a zero duration. -func sync(r ReadableKeySetRepo, w WritableKeySetRepo, clock clockwork.Clock) (exp time.Duration, err error) { +func syncKeySet(r ReadableKeySetRepo, w WritableKeySetRepo, clock clockwork.Clock) (exp time.Duration, err error) { var ks KeySet ks, err = r.Get() if err != nil { diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/oauth2/oauth2.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/oauth2/oauth2.go index c5583e518fe..14bd6cd3f5e 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/oauth2/oauth2.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/oauth2/oauth2.go @@ -8,14 +8,49 @@ import ( "mime" "net/http" "net/url" + "sort" "strconv" "strings" phttp "github.com/coreos/go-oidc/http" ) +// ResponseTypesEqual compares two response_type values. If either +// contains a space, it is treated as an unordered list. For example, +// comparing "code id_token" and "id_token code" would evaluate to true. +func ResponseTypesEqual(r1, r2 string) bool { + if !strings.Contains(r1, " ") || !strings.Contains(r2, " ") { + // fast route, no split needed + return r1 == r2 + } + + // split, sort, and compare + r1Fields := strings.Fields(r1) + r2Fields := strings.Fields(r2) + if len(r1Fields) != len(r2Fields) { + return false + } + sort.Strings(r1Fields) + sort.Strings(r2Fields) + for i, r1Field := range r1Fields { + if r1Field != r2Fields[i] { + return false + } + } + return true +} + const ( - ResponseTypeCode = "code" + // OAuth2.0 response types registered by OIDC. + // + // See: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#RegistryContents + ResponseTypeCode = "code" + ResponseTypeCodeIDToken = "code id_token" + ResponseTypeCodeIDTokenToken = "code id_token token" + ResponseTypeIDToken = "id_token" + ResponseTypeIDTokenToken = "id_token token" + ResponseTypeToken = "token" + ResponseTypeNone = "none" ) const ( @@ -136,22 +171,24 @@ func (c *Client) commonURLValues() url.Values { } } -func (c *Client) newAuthenticatedRequest(url string, values url.Values) (*http.Request, error) { +func (c *Client) newAuthenticatedRequest(urlToken string, values url.Values) (*http.Request, error) { var req *http.Request var err error switch c.authMethod { case AuthMethodClientSecretPost: values.Set("client_secret", c.creds.Secret) - req, err = http.NewRequest("POST", url, strings.NewReader(values.Encode())) + req, err = http.NewRequest("POST", urlToken, strings.NewReader(values.Encode())) if err != nil { return nil, err } case AuthMethodClientSecretBasic: - req, err = http.NewRequest("POST", url, strings.NewReader(values.Encode())) + req, err = http.NewRequest("POST", urlToken, strings.NewReader(values.Encode())) if err != nil { return nil, err } - req.SetBasicAuth(c.creds.ID, c.creds.Secret) + encodedID := url.QueryEscape(c.creds.ID) + encodedSecret := url.QueryEscape(c.creds.Secret) + req.SetBasicAuth(encodedID, encodedSecret) default: panic("misconfigured client: auth method not supported") } diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/client.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/client.go index 76330237f1f..7a3cb40f645 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/client.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/client.go @@ -1,9 +1,11 @@ package oidc import ( + "encoding/json" "errors" "fmt" "net/http" + "net/mail" "net/url" "sync" "time" @@ -36,23 +38,520 @@ type ClientIdentity struct { Metadata ClientMetadata } -type ClientMetadata struct { - RedirectURLs []url.URL +type JWAOptions struct { + // SigningAlg specifies an JWA alg for signing JWTs. + // + // Specifying this field implies different actions depending on the context. It may + // require objects be serialized and signed as a JWT instead of plain JSON, or + // require an existing JWT object use the specified alg. + // + // See: http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata + SigningAlg string + // EncryptionAlg, if provided, specifies that the returned or sent object be stored + // (or nested) within a JWT object and encrypted with the provided JWA alg. + EncryptionAlg string + // EncryptionEnc specifies the JWA enc algorithm to use with EncryptionAlg. If + // EncryptionAlg is provided and EncryptionEnc is omitted, this field defaults + // to A128CBC-HS256. + // + // If EncryptionEnc is provided EncryptionAlg must also be specified. + EncryptionEnc string } +func (opt JWAOptions) valid() error { + if opt.EncryptionEnc != "" && opt.EncryptionAlg == "" { + return errors.New("encryption encoding provided with no encryption algorithm") + } + return nil +} + +func (opt JWAOptions) defaults() JWAOptions { + if opt.EncryptionAlg != "" && opt.EncryptionEnc == "" { + opt.EncryptionEnc = jose.EncA128CBCHS256 + } + return opt +} + +var ( + // Ensure ClientMetadata satisfies these interfaces. + _ json.Marshaler = &ClientMetadata{} + _ json.Unmarshaler = &ClientMetadata{} +) + +// ClientMetadata holds metadata that the authorization server associates +// with a client identifier. The fields range from human-facing display +// strings such as client name, to items that impact the security of the +// protocol, such as the list of valid redirect URIs. +// +// See http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata +// +// TODO: support language specific claim representations +// http://openid.net/specs/openid-connect-registration-1_0.html#LanguagesAndScripts +type ClientMetadata struct { + RedirectURIs []url.URL // Required + + // A list of OAuth 2.0 "response_type" values that the client wishes to restrict + // itself to. Either "code", "token", or another registered extension. + // + // If omitted, only "code" will be used. + ResponseTypes []string + // A list of OAuth 2.0 grant types the client wishes to restrict itself to. + // The grant type values used by OIDC are "authorization_code", "implicit", + // and "refresh_token". + // + // If ommitted, only "authorization_code" will be used. + GrantTypes []string + // "native" or "web". If omitted, "web". + ApplicationType string + + // List of email addresses. + Contacts []mail.Address + // Name of client to be presented to the end-user. + ClientName string + // URL that references a logo for the Client application. + LogoURI *url.URL + // URL of the home page of the Client. + ClientURI *url.URL + // Profile data policies and terms of use to be provided to the end user. + PolicyURI *url.URL + TermsOfServiceURI *url.URL + + // URL to or the value of the client's JSON Web Key Set document. + JWKSURI *url.URL + JWKS *jose.JWKSet + + // URL referencing a flie with a single JSON array of redirect URIs. + SectorIdentifierURI *url.URL + + SubjectType string + + // Options to restrict the JWS alg and enc values used for server responses and requests. + IDTokenResponseOptions JWAOptions + UserInfoResponseOptions JWAOptions + RequestObjectOptions JWAOptions + + // Client requested authorization method and signing options for the token endpoint. + // + // Defaults to "client_secret_basic" + TokenEndpointAuthMethod string + TokenEndpointAuthSigningAlg string + + // DefaultMaxAge specifies the maximum amount of time in seconds before an authorized + // user must reauthroize. + // + // If 0, no limitation is placed on the maximum. + DefaultMaxAge int64 + // RequireAuthTime specifies if the auth_time claim in the ID token is required. + RequireAuthTime bool + + // Default Authentication Context Class Reference values for authentication requests. + DefaultACRValues []string + + // URI that a third party can use to initiate a login by the relaying party. + // + // See: http://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin + InitiateLoginURI *url.URL + // Pre-registered request_uri values that may be cached by the server. + RequestURIs []url.URL +} + +// Defaults returns a shallow copy of ClientMetadata with default +// values replacing omitted fields. +func (m ClientMetadata) Defaults() ClientMetadata { + if len(m.ResponseTypes) == 0 { + m.ResponseTypes = []string{oauth2.ResponseTypeCode} + } + if len(m.GrantTypes) == 0 { + m.GrantTypes = []string{oauth2.GrantTypeAuthCode} + } + if m.ApplicationType == "" { + m.ApplicationType = "web" + } + if m.TokenEndpointAuthMethod == "" { + m.TokenEndpointAuthMethod = oauth2.AuthMethodClientSecretBasic + } + m.IDTokenResponseOptions = m.IDTokenResponseOptions.defaults() + m.UserInfoResponseOptions = m.UserInfoResponseOptions.defaults() + m.RequestObjectOptions = m.RequestObjectOptions.defaults() + return m +} + +func (m *ClientMetadata) MarshalJSON() ([]byte, error) { + e := m.toEncodableStruct() + return json.Marshal(&e) +} + +func (m *ClientMetadata) UnmarshalJSON(data []byte) error { + var e encodableClientMetadata + if err := json.Unmarshal(data, &e); err != nil { + return err + } + meta, err := e.toStruct() + if err != nil { + return err + } + if err := meta.Valid(); err != nil { + return err + } + *m = meta + return nil +} + +type encodableClientMetadata struct { + RedirectURIs []string `json:"redirect_uris"` // Required + ResponseTypes []string `json:"response_types,omitempty"` + GrantTypes []string `json:"grant_types,omitempty"` + ApplicationType string `json:"application_type,omitempty"` + Contacts []string `json:"contacts,omitempty"` + ClientName string `json:"client_name,omitempty"` + LogoURI string `json:"logo_uri,omitempty"` + ClientURI string `json:"client_uri,omitempty"` + PolicyURI string `json:"policy_uri,omitempty"` + TermsOfServiceURI string `json:"tos_uri,omitempty"` + JWKSURI string `json:"jwks_uri,omitempty"` + JWKS *jose.JWKSet `json:"jwks,omitempty"` + SectorIdentifierURI string `json:"sector_identifier_uri,omitempty"` + SubjectType string `json:"subject_type,omitempty"` + IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty"` + IDTokenEncryptedResponseAlg string `json:"id_token_encrypted_response_alg,omitempty"` + IDTokenEncryptedResponseEnc string `json:"id_token_encrypted_response_enc,omitempty"` + UserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty"` + UserInfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty"` + UserInfoEncryptedResponseEnc string `json:"userinfo_encrypted_response_enc,omitempty"` + RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty"` + RequestObjectEncryptionAlg string `json:"request_object_encryption_alg,omitempty"` + RequestObjectEncryptionEnc string `json:"request_object_encryption_enc,omitempty"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` + TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty"` + DefaultMaxAge int64 `json:"default_max_age,omitempty"` + RequireAuthTime bool `json:"require_auth_time,omitempty"` + DefaultACRValues []string `json:"default_acr_values,omitempty"` + InitiateLoginURI string `json:"initiate_login_uri,omitempty"` + RequestURIs []string `json:"request_uris,omitempty"` +} + +func (c *encodableClientMetadata) toStruct() (ClientMetadata, error) { + p := stickyErrParser{} + m := ClientMetadata{ + RedirectURIs: p.parseURIs(c.RedirectURIs, "redirect_uris"), + ResponseTypes: c.ResponseTypes, + GrantTypes: c.GrantTypes, + ApplicationType: c.ApplicationType, + Contacts: p.parseEmails(c.Contacts, "contacts"), + ClientName: c.ClientName, + LogoURI: p.parseURI(c.LogoURI, "logo_uri"), + ClientURI: p.parseURI(c.ClientURI, "client_uri"), + PolicyURI: p.parseURI(c.PolicyURI, "policy_uri"), + TermsOfServiceURI: p.parseURI(c.TermsOfServiceURI, "tos_uri"), + JWKSURI: p.parseURI(c.JWKSURI, "jwks_uri"), + JWKS: c.JWKS, + SectorIdentifierURI: p.parseURI(c.SectorIdentifierURI, "sector_identifier_uri"), + SubjectType: c.SubjectType, + TokenEndpointAuthMethod: c.TokenEndpointAuthMethod, + TokenEndpointAuthSigningAlg: c.TokenEndpointAuthSigningAlg, + DefaultMaxAge: c.DefaultMaxAge, + RequireAuthTime: c.RequireAuthTime, + DefaultACRValues: c.DefaultACRValues, + InitiateLoginURI: p.parseURI(c.InitiateLoginURI, "initiate_login_uri"), + RequestURIs: p.parseURIs(c.RequestURIs, "request_uris"), + IDTokenResponseOptions: JWAOptions{ + c.IDTokenSignedResponseAlg, + c.IDTokenEncryptedResponseAlg, + c.IDTokenEncryptedResponseEnc, + }, + UserInfoResponseOptions: JWAOptions{ + c.UserInfoSignedResponseAlg, + c.UserInfoEncryptedResponseAlg, + c.UserInfoEncryptedResponseEnc, + }, + RequestObjectOptions: JWAOptions{ + c.RequestObjectSigningAlg, + c.RequestObjectEncryptionAlg, + c.RequestObjectEncryptionEnc, + }, + } + if p.firstErr != nil { + return ClientMetadata{}, p.firstErr + } + return m, nil +} + +// stickyErrParser parses URIs and email addresses. Once it encounters +// a parse error, subsequent calls become no-op. +type stickyErrParser struct { + firstErr error +} + +func (p *stickyErrParser) parseURI(s, field string) *url.URL { + if p.firstErr != nil || s == "" { + return nil + } + u, err := url.Parse(s) + if err == nil { + if u.Host == "" { + err = errors.New("no host in URI") + } else if u.Scheme != "http" && u.Scheme != "https" { + err = errors.New("invalid URI scheme") + } + } + if err != nil { + p.firstErr = fmt.Errorf("failed to parse %s: %v", field, err) + return nil + } + return u +} + +func (p *stickyErrParser) parseURIs(s []string, field string) []url.URL { + if p.firstErr != nil || len(s) == 0 { + return nil + } + uris := make([]url.URL, len(s)) + for i, val := range s { + if val == "" { + p.firstErr = fmt.Errorf("invalid URI in field %s", field) + return nil + } + if u := p.parseURI(val, field); u != nil { + uris[i] = *u + } + } + return uris +} + +func (p *stickyErrParser) parseEmails(s []string, field string) []mail.Address { + if p.firstErr != nil || len(s) == 0 { + return nil + } + addrs := make([]mail.Address, len(s)) + for i, addr := range s { + if addr == "" { + p.firstErr = fmt.Errorf("invalid email in field %s", field) + return nil + } + a, err := mail.ParseAddress(addr) + if err != nil { + p.firstErr = fmt.Errorf("invalid email in field %s: %v", field, err) + return nil + } + addrs[i] = *a + } + return addrs +} + +func (m *ClientMetadata) toEncodableStruct() encodableClientMetadata { + return encodableClientMetadata{ + RedirectURIs: urisToStrings(m.RedirectURIs), + ResponseTypes: m.ResponseTypes, + GrantTypes: m.GrantTypes, + ApplicationType: m.ApplicationType, + Contacts: emailsToStrings(m.Contacts), + ClientName: m.ClientName, + LogoURI: uriToString(m.LogoURI), + ClientURI: uriToString(m.ClientURI), + PolicyURI: uriToString(m.PolicyURI), + TermsOfServiceURI: uriToString(m.TermsOfServiceURI), + JWKSURI: uriToString(m.JWKSURI), + JWKS: m.JWKS, + SectorIdentifierURI: uriToString(m.SectorIdentifierURI), + SubjectType: m.SubjectType, + IDTokenSignedResponseAlg: m.IDTokenResponseOptions.SigningAlg, + IDTokenEncryptedResponseAlg: m.IDTokenResponseOptions.EncryptionAlg, + IDTokenEncryptedResponseEnc: m.IDTokenResponseOptions.EncryptionEnc, + UserInfoSignedResponseAlg: m.UserInfoResponseOptions.SigningAlg, + UserInfoEncryptedResponseAlg: m.UserInfoResponseOptions.EncryptionAlg, + UserInfoEncryptedResponseEnc: m.UserInfoResponseOptions.EncryptionEnc, + RequestObjectSigningAlg: m.RequestObjectOptions.SigningAlg, + RequestObjectEncryptionAlg: m.RequestObjectOptions.EncryptionAlg, + RequestObjectEncryptionEnc: m.RequestObjectOptions.EncryptionEnc, + TokenEndpointAuthMethod: m.TokenEndpointAuthMethod, + TokenEndpointAuthSigningAlg: m.TokenEndpointAuthSigningAlg, + DefaultMaxAge: m.DefaultMaxAge, + RequireAuthTime: m.RequireAuthTime, + DefaultACRValues: m.DefaultACRValues, + InitiateLoginURI: uriToString(m.InitiateLoginURI), + RequestURIs: urisToStrings(m.RequestURIs), + } +} + +func uriToString(u *url.URL) string { + if u == nil { + return "" + } + return u.String() +} + +func urisToStrings(urls []url.URL) []string { + if len(urls) == 0 { + return nil + } + sli := make([]string, len(urls)) + for i, u := range urls { + sli[i] = u.String() + } + return sli +} + +func emailsToStrings(addrs []mail.Address) []string { + if len(addrs) == 0 { + return nil + } + sli := make([]string, len(addrs)) + for i, addr := range addrs { + sli[i] = addr.String() + } + return sli +} + +// Valid determines if a ClientMetadata conforms with the OIDC specification. +// +// Valid is called by UnmarshalJSON. +// +// NOTE(ericchiang): For development purposes Valid does not mandate 'https' for +// URLs fields where the OIDC spec requires it. This may change in future releases +// of this package. See: https://github.com/coreos/go-oidc/issues/34 func (m *ClientMetadata) Valid() error { - if len(m.RedirectURLs) == 0 { + if len(m.RedirectURIs) == 0 { return errors.New("zero redirect URLs") } - for _, u := range m.RedirectURLs { + validURI := func(u *url.URL, fieldName string) error { + if u.Host == "" { + return fmt.Errorf("no host for uri field %s", fieldName) + } if u.Scheme != "http" && u.Scheme != "https" { - return errors.New("invalid redirect URL: scheme not http/https") - } else if u.Host == "" { - return errors.New("invalid redirect URL: host empty") + return fmt.Errorf("uri field %s scheme is not http or https", fieldName) + } + return nil + } + + uris := []struct { + val *url.URL + name string + }{ + {m.LogoURI, "logo_uri"}, + {m.ClientURI, "client_uri"}, + {m.PolicyURI, "policy_uri"}, + {m.TermsOfServiceURI, "tos_uri"}, + {m.JWKSURI, "jwks_uri"}, + {m.SectorIdentifierURI, "sector_identifier_uri"}, + {m.InitiateLoginURI, "initiate_login_uri"}, + } + + for _, uri := range uris { + if uri.val == nil { + continue + } + if err := validURI(uri.val, uri.name); err != nil { + return err } } + uriLists := []struct { + vals []url.URL + name string + }{ + {m.RedirectURIs, "redirect_uris"}, + {m.RequestURIs, "request_uris"}, + } + for _, list := range uriLists { + for _, uri := range list.vals { + if err := validURI(&uri, list.name); err != nil { + return err + } + } + } + + options := []struct { + option JWAOptions + name string + }{ + {m.IDTokenResponseOptions, "id_token response"}, + {m.UserInfoResponseOptions, "userinfo response"}, + {m.RequestObjectOptions, "request_object"}, + } + for _, option := range options { + if err := option.option.valid(); err != nil { + return fmt.Errorf("invalid JWA values for %s: %v", option.name, err) + } + } + return nil +} + +type ClientRegistrationResponse struct { + ClientID string // Required + ClientSecret string + RegistrationAccessToken string + RegistrationClientURI string + // If IsZero is true, unspecified. + ClientIDIssuedAt time.Time + // Time at which the client_secret will expire. + // If IsZero is true, it will not expire. + ClientSecretExpiresAt time.Time + + ClientMetadata +} + +type encodableClientRegistrationResponse struct { + ClientID string `json:"client_id"` // Required + ClientSecret string `json:"client_secret,omitempty"` + RegistrationAccessToken string `json:"registration_access_token,omitempty"` + RegistrationClientURI string `json:"registration_client_uri,omitempty"` + ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"` + // Time at which the client_secret will expire, in seconds since the epoch. + // If 0 it will not expire. + ClientSecretExpiresAt int64 `json:"client_secret_expires_at"` // Required + + encodableClientMetadata +} + +func unixToSec(t time.Time) int64 { + if t.IsZero() { + return 0 + } + return t.Unix() +} + +func (c *ClientRegistrationResponse) MarshalJSON() ([]byte, error) { + e := encodableClientRegistrationResponse{ + ClientID: c.ClientID, + ClientSecret: c.ClientSecret, + RegistrationAccessToken: c.RegistrationAccessToken, + RegistrationClientURI: c.RegistrationClientURI, + ClientIDIssuedAt: unixToSec(c.ClientIDIssuedAt), + ClientSecretExpiresAt: unixToSec(c.ClientSecretExpiresAt), + encodableClientMetadata: c.ClientMetadata.toEncodableStruct(), + } + return json.Marshal(&e) +} + +func secToUnix(sec int64) time.Time { + if sec == 0 { + return time.Time{} + } + return time.Unix(sec, 0) +} + +func (c *ClientRegistrationResponse) UnmarshalJSON(data []byte) error { + var e encodableClientRegistrationResponse + if err := json.Unmarshal(data, &e); err != nil { + return err + } + if e.ClientID == "" { + return errors.New("no client_id in client registration response") + } + metadata, err := e.encodableClientMetadata.toStruct() + if err != nil { + return err + } + *c = ClientRegistrationResponse{ + ClientID: e.ClientID, + ClientSecret: e.ClientSecret, + RegistrationAccessToken: e.RegistrationAccessToken, + RegistrationClientURI: e.RegistrationClientURI, + ClientIDIssuedAt: secToUnix(e.ClientIDIssuedAt), + ClientSecretExpiresAt: secToUnix(e.ClientSecretExpiresAt), + ClientMetadata: metadata, + } return nil } @@ -101,34 +600,12 @@ type Client struct { redirectURL string scope []string keySet key.PublicKeySet + providerSyncer *ProviderConfigSyncer keySetSyncMutex sync.RWMutex lastKeySetSync time.Time } -type providerConfigRepo struct { - mu sync.RWMutex - config ProviderConfig // do not access directly, use Get() -} - -func newProviderConfigRepo(pc ProviderConfig) *providerConfigRepo { - return &providerConfigRepo{sync.RWMutex{}, pc} -} - -// returns an error to implement ProviderConfigSetter -func (r *providerConfigRepo) Set(cfg ProviderConfig) error { - r.mu.Lock() - defer r.mu.Unlock() - r.config = cfg - return nil -} - -func (r *providerConfigRepo) Get() ProviderConfig { - r.mu.RLock() - defer r.mu.RUnlock() - return r.config -} - func (c *Client) Healthy() error { now := time.Now().UTC() @@ -155,8 +632,8 @@ func (c *Client) OAuthClient() (*oauth2.Client, error) { ocfg := oauth2.Config{ Credentials: oauth2.ClientCredentials(c.credentials), RedirectURL: c.redirectURL, - AuthURL: cfg.AuthEndpoint, - TokenURL: cfg.TokenEndpoint, + AuthURL: cfg.AuthEndpoint.String(), + TokenURL: cfg.TokenEndpoint.String(), Scope: c.scope, AuthMethod: authMethod, } @@ -178,9 +655,13 @@ func chooseAuthMethod(cfg ProviderConfig) (string, error) { return "", errors.New("no supported auth methods") } +// SyncProviderConfig starts the provider config syncer func (c *Client) SyncProviderConfig(discoveryURL string) chan struct{} { r := NewHTTPProviderConfigGetter(c.httpClient, discoveryURL) - return NewProviderConfigSyncer(r, c.providerConfig).Run() + s := NewProviderConfigSyncer(r, c.providerConfig) + stop := s.Run() + s.WaitUntilInitialSync() + return stop } func (c *Client) maybeSyncKeys() error { @@ -204,7 +685,7 @@ func (c *Client) maybeSyncKeys() error { } cfg := c.providerConfig.Get() - r := NewRemotePublicKeyRepo(c.httpClient, cfg.KeysEndpoint) + r := NewRemotePublicKeyRepo(c.httpClient, cfg.KeysEndpoint.String()) w := &clientKeyRepo{client: c} _, err := key.Sync(r, w) c.lastKeySetSync = time.Now().UTC() @@ -299,7 +780,7 @@ func (c *Client) VerifyJWT(jwt jose.JWT) error { } v := NewJWTVerifier( - c.providerConfig.Get().Issuer, + c.providerConfig.Get().Issuer.String(), c.credentials.ID, c.maybeSyncKeys, keysFunc) @@ -340,3 +821,26 @@ func (c *Client) keysFuncAll() func() []key.PublicKey { return c.keySet.Keys() } } + +type providerConfigRepo struct { + mu sync.RWMutex + config ProviderConfig // do not access directly, use Get() +} + +func newProviderConfigRepo(pc ProviderConfig) *providerConfigRepo { + return &providerConfigRepo{sync.RWMutex{}, pc} +} + +// returns an error to implement ProviderConfigSetter +func (r *providerConfigRepo) Set(cfg ProviderConfig) error { + r.mu.Lock() + defer r.mu.Unlock() + r.config = cfg + return nil +} + +func (r *providerConfigRepo) Get() ProviderConfig { + r.mu.RLock() + defer r.mu.RUnlock() + return r.config +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/provider.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/provider.go index f911e39e08c..807cf00adec 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/provider.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/provider.go @@ -2,8 +2,11 @@ package oidc import ( "encoding/json" + "errors" "fmt" "net/http" + "net/url" + "sync" "time" "github.com/coreos/pkg/capnslog" @@ -18,6 +21,26 @@ var ( log = capnslog.NewPackageLogger("github.com/coreos/go-oidc", "http") ) +const ( + // Subject Identifier types defined by the OIDC spec. Specifies if the provider + // should provide the same sub claim value to all clients (public) or a unique + // value for each client (pairwise). + // + // See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes + SubjectTypePublic = "public" + SubjectTypePairwise = "pairwise" +) + +var ( + // Default values for omitted provider config fields. + // + // Use ProviderConfig's Defaults method to fill a provider config with these values. + DefaultGrantTypesSupported = []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeImplicit} + DefaultResponseModesSupported = []string{"query", "fragment"} + DefaultTokenEndpointAuthMethodsSupported = []string{oauth2.AuthMethodClientSecretBasic} + DefaultClaimTypesSupported = []string{"normal"} +) + const ( MaximumProviderConfigSyncInterval = 24 * time.Hour MinimumProviderConfigSyncInterval = time.Minute @@ -28,29 +51,414 @@ const ( // internally configurable for tests var minimumProviderConfigSyncInterval = MinimumProviderConfigSyncInterval +var ( + // Ensure ProviderConfig satisfies these interfaces. + _ json.Marshaler = &ProviderConfig{} + _ json.Unmarshaler = &ProviderConfig{} +) + +// ProviderConfig represents the OpenID Provider Metadata specifying what +// configurations a provider supports. +// +// See: http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata type ProviderConfig struct { - Issuer string `json:"issuer"` - AuthEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` - KeysEndpoint string `json:"jwks_uri"` - ResponseTypesSupported []string `json:"response_types_supported"` - GrantTypesSupported []string `json:"grant_types_supported"` - SubjectTypesSupported []string `json:"subject_types_supported"` - IDTokenAlgValuesSupported []string `json:"id_token_alg_values_supported"` - TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"` - ExpiresAt time.Time `json:"-"` + Issuer *url.URL // Required + AuthEndpoint *url.URL // Required + TokenEndpoint *url.URL // Required if grant types other than "implicit" are supported + UserInfoEndpoint *url.URL + KeysEndpoint *url.URL // Required + RegistrationEndpoint *url.URL + + // Servers MAY choose not to advertise some supported scope values even when this + // parameter is used, although those defined in OpenID Core SHOULD be listed, if supported. + ScopesSupported []string + // OAuth2.0 response types supported. + ResponseTypesSupported []string // Required + // OAuth2.0 response modes supported. + // + // If omitted, defaults to DefaultResponseModesSupported. + ResponseModesSupported []string + // OAuth2.0 grant types supported. + // + // If omitted, defaults to DefaultGrantTypesSupported. + GrantTypesSupported []string + ACRValuesSupported []string + // SubjectTypesSupported specifies strategies for providing values for the sub claim. + SubjectTypesSupported []string // Required + + // JWA signing and encryption algorith values supported for ID tokens. + IDTokenSigningAlgValues []string // Required + IDTokenEncryptionAlgValues []string + IDTokenEncryptionEncValues []string + + // JWA signing and encryption algorith values supported for user info responses. + UserInfoSigningAlgValues []string + UserInfoEncryptionAlgValues []string + UserInfoEncryptionEncValues []string + + // JWA signing and encryption algorith values supported for request objects. + ReqObjSigningAlgValues []string + ReqObjEncryptionAlgValues []string + ReqObjEncryptionEncValues []string + + TokenEndpointAuthMethodsSupported []string + TokenEndpointAuthSigningAlgValuesSupported []string + DisplayValuesSupported []string + ClaimTypesSupported []string + ClaimsSupported []string + ServiceDocs *url.URL + ClaimsLocalsSupported []string + UILocalsSupported []string + ClaimsParameterSupported bool + RequestParameterSupported bool + RequestURIParamaterSupported bool + RequireRequestURIRegistration bool + + Policy *url.URL + TermsOfService *url.URL + + // Not part of the OpenID Provider Metadata + ExpiresAt time.Time } +// Defaults returns a shallow copy of ProviderConfig with default +// values replacing omitted fields. +// +// var cfg oidc.ProviderConfig +// // Fill provider config with default values for omitted fields. +// cfg = cfg.Defaults() +// +func (p ProviderConfig) Defaults() ProviderConfig { + setDefault := func(val *[]string, defaultVal []string) { + if len(*val) == 0 { + *val = defaultVal + } + } + setDefault(&p.GrantTypesSupported, DefaultGrantTypesSupported) + setDefault(&p.ResponseModesSupported, DefaultResponseModesSupported) + setDefault(&p.TokenEndpointAuthMethodsSupported, DefaultTokenEndpointAuthMethodsSupported) + setDefault(&p.ClaimTypesSupported, DefaultClaimTypesSupported) + return p +} + +func (p *ProviderConfig) MarshalJSON() ([]byte, error) { + e := p.toEncodableStruct() + return json.Marshal(&e) +} + +func (p *ProviderConfig) UnmarshalJSON(data []byte) error { + var e encodableProviderConfig + if err := json.Unmarshal(data, &e); err != nil { + return err + } + conf, err := e.toStruct() + if err != nil { + return err + } + if err := conf.Valid(); err != nil { + return err + } + *p = conf + return nil +} + +type encodableProviderConfig struct { + Issuer string `json:"issuer"` + AuthEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + UserInfoEndpoint string `json:"userinfo_endpoint,omitempty"` + KeysEndpoint string `json:"jwks_uri"` + RegistrationEndpoint string `json:"registration_endpoint,omitempty"` + + // Use 'omitempty' for all slices as per OIDC spec: + // "Claims that return multiple values are represented as JSON arrays. + // Claims with zero elements MUST be omitted from the response." + // http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse + + ScopesSupported []string `json:"scopes_supported,omitempty"` + ResponseTypesSupported []string `json:"response_types_supported,omitempty"` + ResponseModesSupported []string `json:"response_modes_supported,omitempty"` + GrantTypesSupported []string `json:"grant_types_supported,omitempty"` + ACRValuesSupported []string `json:"acr_values_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + + IDTokenSigningAlgValues []string `json:"id_token_signing_alg_values_supported,omitempty"` + IDTokenEncryptionAlgValues []string `json:"id_token_encryption_alg_values_supported,omitempty"` + IDTokenEncryptionEncValues []string `json:"id_token_encryption_enc_values_supported,omitempty"` + UserInfoSigningAlgValues []string `json:"userinfo_signing_alg_values_supported,omitempty"` + UserInfoEncryptionAlgValues []string `json:"userinfo_encryption_alg_values_supported,omitempty"` + UserInfoEncryptionEncValues []string `json:"userinfo_encryption_enc_values_supported,omitempty"` + ReqObjSigningAlgValues []string `json:"request_object_signing_alg_values_supported,omitempty"` + ReqObjEncryptionAlgValues []string `json:"request_object_encryption_alg_values_supported,omitempty"` + ReqObjEncryptionEncValues []string `json:"request_object_encryption_enc_values_supported,omitempty"` + + TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` + TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` + + DisplayValuesSupported []string `json:"display_values_supported,omitempty"` + ClaimTypesSupported []string `json:"claim_types_supported,omitempty"` + ClaimsSupported []string `json:"claims_supported,omitempty"` + ServiceDocs string `json:"service_documentation,omitempty"` + ClaimsLocalsSupported []string `json:"claims_locales_supported,omitempty"` + UILocalsSupported []string `json:"ui_locales_supported,omitempty"` + ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"` + RequestParameterSupported bool `json:"request_parameter_supported,omitempty"` + RequestURIParamaterSupported bool `json:"request_uri_parameter_supported,omitempty"` + RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"` + + Policy string `json:"op_policy_uri,omitempty"` + TermsOfService string `json:"op_tos_uri,omitempty"` +} + +func (cfg ProviderConfig) toEncodableStruct() encodableProviderConfig { + return encodableProviderConfig{ + Issuer: uriToString(cfg.Issuer), + AuthEndpoint: uriToString(cfg.AuthEndpoint), + TokenEndpoint: uriToString(cfg.TokenEndpoint), + UserInfoEndpoint: uriToString(cfg.UserInfoEndpoint), + KeysEndpoint: uriToString(cfg.KeysEndpoint), + RegistrationEndpoint: uriToString(cfg.RegistrationEndpoint), + ScopesSupported: cfg.ScopesSupported, + ResponseTypesSupported: cfg.ResponseTypesSupported, + ResponseModesSupported: cfg.ResponseModesSupported, + GrantTypesSupported: cfg.GrantTypesSupported, + ACRValuesSupported: cfg.ACRValuesSupported, + SubjectTypesSupported: cfg.SubjectTypesSupported, + IDTokenSigningAlgValues: cfg.IDTokenSigningAlgValues, + IDTokenEncryptionAlgValues: cfg.IDTokenEncryptionAlgValues, + IDTokenEncryptionEncValues: cfg.IDTokenEncryptionEncValues, + UserInfoSigningAlgValues: cfg.UserInfoSigningAlgValues, + UserInfoEncryptionAlgValues: cfg.UserInfoEncryptionAlgValues, + UserInfoEncryptionEncValues: cfg.UserInfoEncryptionEncValues, + ReqObjSigningAlgValues: cfg.ReqObjSigningAlgValues, + ReqObjEncryptionAlgValues: cfg.ReqObjEncryptionAlgValues, + ReqObjEncryptionEncValues: cfg.ReqObjEncryptionEncValues, + TokenEndpointAuthMethodsSupported: cfg.TokenEndpointAuthMethodsSupported, + TokenEndpointAuthSigningAlgValuesSupported: cfg.TokenEndpointAuthSigningAlgValuesSupported, + DisplayValuesSupported: cfg.DisplayValuesSupported, + ClaimTypesSupported: cfg.ClaimTypesSupported, + ClaimsSupported: cfg.ClaimsSupported, + ServiceDocs: uriToString(cfg.ServiceDocs), + ClaimsLocalsSupported: cfg.ClaimsLocalsSupported, + UILocalsSupported: cfg.UILocalsSupported, + ClaimsParameterSupported: cfg.ClaimsParameterSupported, + RequestParameterSupported: cfg.RequestParameterSupported, + RequestURIParamaterSupported: cfg.RequestURIParamaterSupported, + RequireRequestURIRegistration: cfg.RequireRequestURIRegistration, + Policy: uriToString(cfg.Policy), + TermsOfService: uriToString(cfg.TermsOfService), + } +} + +func (e encodableProviderConfig) toStruct() (ProviderConfig, error) { + p := stickyErrParser{} + conf := ProviderConfig{ + Issuer: p.parseURI(e.Issuer, "issuer"), + AuthEndpoint: p.parseURI(e.AuthEndpoint, "authorization_endpoint"), + TokenEndpoint: p.parseURI(e.TokenEndpoint, "token_endpoint"), + UserInfoEndpoint: p.parseURI(e.UserInfoEndpoint, "userinfo_endpoint"), + KeysEndpoint: p.parseURI(e.KeysEndpoint, "jwks_uri"), + RegistrationEndpoint: p.parseURI(e.RegistrationEndpoint, "registration_endpoint"), + ScopesSupported: e.ScopesSupported, + ResponseTypesSupported: e.ResponseTypesSupported, + ResponseModesSupported: e.ResponseModesSupported, + GrantTypesSupported: e.GrantTypesSupported, + ACRValuesSupported: e.ACRValuesSupported, + SubjectTypesSupported: e.SubjectTypesSupported, + IDTokenSigningAlgValues: e.IDTokenSigningAlgValues, + IDTokenEncryptionAlgValues: e.IDTokenEncryptionAlgValues, + IDTokenEncryptionEncValues: e.IDTokenEncryptionEncValues, + UserInfoSigningAlgValues: e.UserInfoSigningAlgValues, + UserInfoEncryptionAlgValues: e.UserInfoEncryptionAlgValues, + UserInfoEncryptionEncValues: e.UserInfoEncryptionEncValues, + ReqObjSigningAlgValues: e.ReqObjSigningAlgValues, + ReqObjEncryptionAlgValues: e.ReqObjEncryptionAlgValues, + ReqObjEncryptionEncValues: e.ReqObjEncryptionEncValues, + TokenEndpointAuthMethodsSupported: e.TokenEndpointAuthMethodsSupported, + TokenEndpointAuthSigningAlgValuesSupported: e.TokenEndpointAuthSigningAlgValuesSupported, + DisplayValuesSupported: e.DisplayValuesSupported, + ClaimTypesSupported: e.ClaimTypesSupported, + ClaimsSupported: e.ClaimsSupported, + ServiceDocs: p.parseURI(e.ServiceDocs, "service_documentation"), + ClaimsLocalsSupported: e.ClaimsLocalsSupported, + UILocalsSupported: e.UILocalsSupported, + ClaimsParameterSupported: e.ClaimsParameterSupported, + RequestParameterSupported: e.RequestParameterSupported, + RequestURIParamaterSupported: e.RequestURIParamaterSupported, + RequireRequestURIRegistration: e.RequireRequestURIRegistration, + Policy: p.parseURI(e.Policy, "op_policy-uri"), + TermsOfService: p.parseURI(e.TermsOfService, "op_tos_uri"), + } + if p.firstErr != nil { + return ProviderConfig{}, p.firstErr + } + return conf, nil +} + +// Empty returns if a ProviderConfig holds no information. +// +// This case generally indicates a ProviderConfigGetter has experienced an error +// and has nothing to report. func (p ProviderConfig) Empty() bool { - return p.Issuer == "" + return p.Issuer == nil +} + +func contains(sli []string, ele string) bool { + for _, s := range sli { + if s == ele { + return true + } + } + return false +} + +// Valid determines if a ProviderConfig conforms with the OIDC specification. +// If Valid returns successfully it guarantees required field are non-nil and +// URLs are well formed. +// +// Valid is called by UnmarshalJSON. +// +// NOTE(ericchiang): For development purposes Valid does not mandate 'https' for +// URLs fields where the OIDC spec requires it. This may change in future releases +// of this package. See: https://github.com/coreos/go-oidc/issues/34 +func (p ProviderConfig) Valid() error { + grantTypes := p.GrantTypesSupported + if len(grantTypes) == 0 { + grantTypes = DefaultGrantTypesSupported + } + implicitOnly := true + for _, grantType := range grantTypes { + if grantType != oauth2.GrantTypeImplicit { + implicitOnly = false + break + } + } + + if len(p.SubjectTypesSupported) == 0 { + return errors.New("missing required field subject_types_supported") + } + if len(p.IDTokenSigningAlgValues) == 0 { + return errors.New("missing required field id_token_signing_alg_values_supported") + } + + if len(p.ScopesSupported) != 0 && !contains(p.ScopesSupported, "openid") { + return errors.New("scoped_supported must be unspecified or include 'openid'") + } + + if !contains(p.IDTokenSigningAlgValues, "RS256") { + return errors.New("id_token_signing_alg_values_supported must include 'RS256'") + } + if contains(p.TokenEndpointAuthMethodsSupported, "none") { + return errors.New("token_endpoint_auth_signing_alg_values_supported cannot include 'none'") + } + + uris := []struct { + val *url.URL + name string + required bool + }{ + {p.Issuer, "issuer", true}, + {p.AuthEndpoint, "authorization_endpoint", true}, + {p.TokenEndpoint, "token_endpoint", !implicitOnly}, + {p.UserInfoEndpoint, "userinfo_endpoint", false}, + {p.KeysEndpoint, "jwks_uri", true}, + {p.RegistrationEndpoint, "registration_endpoint", false}, + {p.ServiceDocs, "service_documentation", false}, + {p.Policy, "op_policy_uri", false}, + {p.TermsOfService, "op_tos_uri", false}, + } + + for _, uri := range uris { + if uri.val == nil { + if !uri.required { + continue + } + return fmt.Errorf("empty value for required uri field %s", uri.name) + } + if uri.val.Host == "" { + return fmt.Errorf("no host for uri field %s", uri.name) + } + if uri.val.Scheme != "http" && uri.val.Scheme != "https" { + return fmt.Errorf("uri field %s schemeis not http or https", uri.name) + } + } + return nil +} + +// Supports determines if provider supports a client given their respective metadata. +func (p ProviderConfig) Supports(c ClientMetadata) error { + if err := p.Valid(); err != nil { + return fmt.Errorf("invalid provider config: %v", err) + } + if err := c.Valid(); err != nil { + return fmt.Errorf("invalid client config: %v", err) + } + + // Fill default values for omitted fields + c = c.Defaults() + p = p.Defaults() + + // Do the supported values list the requested one? + supports := []struct { + supported []string + requested string + name string + }{ + {p.IDTokenSigningAlgValues, c.IDTokenResponseOptions.SigningAlg, "id_token_signed_response_alg"}, + {p.IDTokenEncryptionAlgValues, c.IDTokenResponseOptions.EncryptionAlg, "id_token_encryption_response_alg"}, + {p.IDTokenEncryptionEncValues, c.IDTokenResponseOptions.EncryptionEnc, "id_token_encryption_response_enc"}, + {p.UserInfoSigningAlgValues, c.UserInfoResponseOptions.SigningAlg, "userinfo_signed_response_alg"}, + {p.UserInfoEncryptionAlgValues, c.UserInfoResponseOptions.EncryptionAlg, "userinfo_encryption_response_alg"}, + {p.UserInfoEncryptionEncValues, c.UserInfoResponseOptions.EncryptionEnc, "userinfo_encryption_response_enc"}, + {p.ReqObjSigningAlgValues, c.RequestObjectOptions.SigningAlg, "request_object_signing_alg"}, + {p.ReqObjEncryptionAlgValues, c.RequestObjectOptions.EncryptionAlg, "request_object_encryption_alg"}, + {p.ReqObjEncryptionEncValues, c.RequestObjectOptions.EncryptionEnc, "request_object_encryption_enc"}, + } + for _, field := range supports { + if field.requested == "" { + continue + } + if !contains(field.supported, field.requested) { + return fmt.Errorf("provider does not support requested value for field %s", field.name) + } + } + + stringsEqual := func(s1, s2 string) bool { return s1 == s2 } + + // For lists, are the list of requested values a subset of the supported ones? + supportsAll := []struct { + supported []string + requested []string + name string + // OAuth2.0 response_type can be space separated lists where order doesn't matter. + // For example "id_token token" is the same as "token id_token" + // Support a custom compare method. + comp func(s1, s2 string) bool + }{ + {p.GrantTypesSupported, c.GrantTypes, "grant_types", stringsEqual}, + {p.ResponseTypesSupported, c.ResponseTypes, "response_type", oauth2.ResponseTypesEqual}, + } + for _, field := range supportsAll { + requestLoop: + for _, req := range field.requested { + for _, sup := range field.supported { + if field.comp(req, sup) { + continue requestLoop + } + } + return fmt.Errorf("provider does not support requested value for field %s", field.name) + } + } + + // TODO(ericchiang): Are there more checks we feel comfortable with begin strict about? + + return nil } func (p ProviderConfig) SupportsGrantType(grantType string) bool { var supported []string if len(p.GrantTypesSupported) == 0 { - // If omitted, the default value is ["authorization_code", "implicit"]. - // http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata - supported = []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeImplicit} + supported = DefaultGrantTypesSupported } else { supported = p.GrantTypesSupported } @@ -75,6 +483,9 @@ type ProviderConfigSyncer struct { from ProviderConfigGetter to ProviderConfigSetter clock clockwork.Clock + + initialSyncDone bool + initialSyncWait sync.WaitGroup } func NewProviderConfigSyncer(from ProviderConfigGetter, to ProviderConfigSetter) *ProviderConfigSyncer { @@ -91,6 +502,7 @@ func (s *ProviderConfigSyncer) Run() chan struct{} { var next pcsStepper next = &pcsStepNext{aft: time.Duration(0)} + s.initialSyncWait.Add(1) go func() { for { select { @@ -105,6 +517,10 @@ func (s *ProviderConfigSyncer) Run() chan struct{} { return stop } +func (s *ProviderConfigSyncer) WaitUntilInitialSync() { + s.initialSyncWait.Wait() +} + func (s *ProviderConfigSyncer) sync() (time.Duration, error) { cfg, err := s.from.Get() if err != nil { @@ -115,6 +531,11 @@ func (s *ProviderConfigSyncer) sync() (time.Duration, error) { return 0, fmt.Errorf("error setting provider config: %v", err) } + if !s.initialSyncDone { + s.initialSyncWait.Done() + s.initialSyncDone = true + } + log.Infof("Updating provider config: config=%#v", cfg) return nextSyncAfter(cfg.ExpiresAt, s.clock), nil @@ -223,7 +644,7 @@ func (r *httpProviderConfigGetter) Get() (cfg ProviderConfig, err error) { // The issuer value returned MUST be identical to the Issuer URL that was directly used to retrieve the configuration information. // http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation - if !urlEqual(cfg.Issuer, r.issuerURL) { + if !urlEqual(cfg.Issuer.String(), r.issuerURL) { err = fmt.Errorf(`"issuer" in config (%v) does not match provided issuer URL (%v)`, cfg.Issuer, r.issuerURL) return } diff --git a/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/util.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/util.go index 843f9ec0815..f2a5a195e4a 100644 --- a/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/util.go +++ b/Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/util.go @@ -59,8 +59,8 @@ func NewClaims(iss, sub string, aud interface{}, iat, exp time.Time) jose.Claims "iss": iss, "sub": sub, "aud": aud, - "iat": float64(iat.Unix()), - "exp": float64(exp.Unix()), + "iat": iat.Unix(), + "exp": exp.Unix(), } } From 8df55ddbe530c85a4924b5a348dbe29f0285e265 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Tue, 1 Mar 2016 11:37:00 -0800 Subject: [PATCH 2/2] plugin/pkg/auth/authenticator/token/oidc: update test to new go-oidc types The provider config has changed a little bit in go-oidc. It is more complete and now throws errors when unmarshaling provider configs that are missing required fields (as defined by the OpenID Connect Discovery spec). Update the oidc plugin to use the new type. --- .../pkg/auth/authenticator/token/oidc/oidc.go | 4 -- .../authenticator/token/oidc/oidc_test.go | 38 +++++++++++++------ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/plugin/pkg/auth/authenticator/token/oidc/oidc.go b/plugin/pkg/auth/authenticator/token/oidc/oidc.go index 4c042d6115a..c9071b1d723 100644 --- a/plugin/pkg/auth/authenticator/token/oidc/oidc.go +++ b/plugin/pkg/auth/authenticator/token/oidc/oidc.go @@ -99,10 +99,6 @@ func New(issuerURL, clientID, caFile, usernameClaim, groupsClaim string) (*OIDCA glog.Infof("Fetched provider config from %s: %#v", issuerURL, cfg) - if cfg.KeysEndpoint == "" { - return nil, fmt.Errorf("OIDC provider must provide 'jwks_uri' for public key discovery") - } - ccfg := oidc.ClientConfig{ HTTPClient: hc, Credentials: oidc.ClientCredentials{ID: clientID}, diff --git a/plugin/pkg/auth/authenticator/token/oidc/oidc_test.go b/plugin/pkg/auth/authenticator/token/oidc/oidc_test.go index d8260b842f7..a2e3559ce29 100644 --- a/plugin/pkg/auth/authenticator/token/oidc/oidc_test.go +++ b/plugin/pkg/auth/authenticator/token/oidc/oidc_test.go @@ -31,6 +31,7 @@ import ( "net" "net/http" "net/http/httptest" + "net/url" "os" "path" "path/filepath" @@ -70,8 +71,16 @@ func newOIDCProvider(t *testing.T) *oidcProvider { } +func mustParseURL(t *testing.T, s string) *url.URL { + u, err := url.Parse(s) + if err != nil { + t.Fatalf("Failed to parse url: %v", err) + } + return u +} + func (op *oidcProvider) handleConfig(w http.ResponseWriter, req *http.Request) { - b, err := json.Marshal(op.pcfg) + b, err := json.Marshal(&op.pcfg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -203,7 +212,7 @@ func TestOIDCDiscoveryTimeout(t *testing.T) { func TestOIDCDiscoveryNoKeyEndpoint(t *testing.T) { var err error - expectErr := fmt.Errorf("OIDC provider must provide 'jwks_uri' for public key discovery") + expectErr := fmt.Errorf("failed to fetch provider config after 3 retries") cert := path.Join(os.TempDir(), "oidc-cert") key := path.Join(os.TempDir(), "oidc-key") @@ -225,7 +234,7 @@ func TestOIDCDiscoveryNoKeyEndpoint(t *testing.T) { // defer srv.Close() op.pcfg = oidc.ProviderConfig{ - Issuer: srv.URL, + Issuer: mustParseURL(t, srv.URL), // An invalid ProviderConfig. Keys endpoint is required. } _, err = New(srv.URL, "client-foo", cert, "sub", "") @@ -245,8 +254,8 @@ func TestOIDCDiscoverySecureConnection(t *testing.T) { // defer srv.Close() op.pcfg = oidc.ProviderConfig{ - Issuer: srv.URL, - KeysEndpoint: srv.URL + "/keys", + Issuer: mustParseURL(t, srv.URL), + KeysEndpoint: mustParseURL(t, srv.URL+"/keys"), } expectErr := fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", srv.URL, "http") @@ -282,8 +291,8 @@ func TestOIDCDiscoverySecureConnection(t *testing.T) { // defer tlsSrv.Close() op.pcfg = oidc.ProviderConfig{ - Issuer: tlsSrv.URL, - KeysEndpoint: tlsSrv.URL + "/keys", + Issuer: mustParseURL(t, tlsSrv.URL), + KeysEndpoint: mustParseURL(t, tlsSrv.URL+"/keys"), } // Create a client using cert2, should fail. @@ -317,9 +326,15 @@ func TestOIDCAuthentication(t *testing.T) { // TODO: Uncomment when fix #19254 // defer srv.Close() + // A provider config with all required fields. op.pcfg = oidc.ProviderConfig{ - Issuer: srv.URL, - KeysEndpoint: srv.URL + "/keys", + Issuer: mustParseURL(t, srv.URL), + AuthEndpoint: mustParseURL(t, srv.URL+"/auth"), + TokenEndpoint: mustParseURL(t, srv.URL+"/token"), + KeysEndpoint: mustParseURL(t, srv.URL+"/keys"), + ResponseTypesSupported: []string{"code"}, + SubjectTypesSupported: []string{"public"}, + IDTokenSigningAlgValues: []string{"RS256"}, } tests := []struct { @@ -371,7 +386,7 @@ func TestOIDCAuthentication(t *testing.T) { op.generateMalformedToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil), nil, false, - "malformed JWS, unable to decode signature", + "oidc: unable to verify JWT signature: no matching keys", }, { // Invalid 'aud'. @@ -404,7 +419,8 @@ func TestOIDCAuthentication(t *testing.T) { for i, tt := range tests { client, err := New(srv.URL, "client-foo", cert, tt.userClaim, tt.groupsClaim) if err != nil { - t.Fatalf("Unexpected error: %v", err) + t.Errorf("Unexpected error: %v", err) + continue } user, result, err := client.AuthenticateToken(tt.token)