From e80701f4b9b997759ebd33148a6c92a6106a54a5 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Tue, 12 Jan 2016 17:15:55 -0800 Subject: [PATCH 1/4] Godeps: update go-oidc for updates to client and provider metadata --- Godeps/Godeps.json | 10 +- .../github.com/coreos/go-oidc/jose/jose.go | 51 ++ .../src/github.com/coreos/go-oidc/jose/jwk.go | 4 + .../coreos/go-oidc/oauth2/oauth2.go | 37 +- .../github.com/coreos/go-oidc/oidc/client.go | 519 +++++++++++++++++- .../coreos/go-oidc/oidc/provider.go | 437 ++++++++++++++- 6 files changed, 1026 insertions(+), 32 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 7fc40027..6f702bdd 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -16,23 +16,23 @@ }, { "ImportPath": "github.com/coreos/go-oidc/http", - "Rev": "145916abb78708694762ff359ab1e34c47c7947f" + "Rev": "6039032c0b15517897116d333ead8edf38792437" }, { "ImportPath": "github.com/coreos/go-oidc/jose", - "Rev": "145916abb78708694762ff359ab1e34c47c7947f" + "Rev": "6039032c0b15517897116d333ead8edf38792437" }, { "ImportPath": "github.com/coreos/go-oidc/key", - "Rev": "145916abb78708694762ff359ab1e34c47c7947f" + "Rev": "6039032c0b15517897116d333ead8edf38792437" }, { "ImportPath": "github.com/coreos/go-oidc/oauth2", - "Rev": "145916abb78708694762ff359ab1e34c47c7947f" + "Rev": "6039032c0b15517897116d333ead8edf38792437" }, { "ImportPath": "github.com/coreos/go-oidc/oidc", - "Rev": "145916abb78708694762ff359ab1e34c47c7947f" + "Rev": "6039032c0b15517897116d333ead8edf38792437" }, { "ImportPath": "github.com/coreos/pkg/capnslog", 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 4f99aeb7..62099265 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 045f5fde..b7a8e235 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/oauth2/oauth2.go b/Godeps/_workspace/src/github.com/coreos/go-oidc/oauth2/oauth2.go index c5583e51..59493591 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 ( 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 3a73e04a..34cb44bb 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,518 @@ 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 + } + uris[i] = *(p.parseURI(val, field)) + } + 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 } @@ -133,8 +630,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, } @@ -186,7 +683,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() @@ -281,7 +778,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) 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 f2f165fd..807cf00a 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,10 @@ package oidc import ( "encoding/json" + "errors" "fmt" "net/http" + "net/url" "sync" "time" @@ -19,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 @@ -29,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_signing_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 } @@ -237,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 } From 5e44b6bc276b044077d48b59c23911bb73c99291 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Tue, 12 Jan 2016 17:16:28 -0800 Subject: [PATCH 2/4] *: update all to accommodate changes to go-oidc Update dex to comply with the changes to fieldnames and types of the client and provider metadata structs in coreos/go-oidc. --- client/client.go | 4 ++-- client/client_test.go | 14 ++++++------- cmd/dexctl/command_client.go | 2 +- cmd/dexctl/driver_api.go | 8 ++++---- cmd/dexctl/driver_db.go | 2 +- connector/connector_oidc_test.go | 4 ++-- functional/db_test.go | 12 +++++------ functional/repo/client_repo_test.go | 4 ++-- integration/client_api_test.go | 4 ++-- integration/user_api_test.go | 4 ++-- schema/workerschema/mapper.go | 12 +++++------ server/client_resource.go | 2 +- server/client_resource_test.go | 10 ++++----- server/email_verification.go | 2 +- server/http.go | 6 +++--- server/http_test.go | 21 +++++++++++-------- server/password.go | 2 +- server/server.go | 21 +++++++++++-------- server/server_test.go | 32 +++++++++++++++-------------- server/testutil.go | 2 +- user/api/api.go | 2 +- user/api/api_test.go | 2 +- 22 files changed, 92 insertions(+), 80 deletions(-) diff --git a/client/client.go b/client/client.go index d08e73e6..ad932404 100644 --- a/client/client.go +++ b/client/client.go @@ -172,7 +172,7 @@ func (ci *clientIdentity) UnmarshalJSON(data []byte) error { Secret: c.Secret, } ci.Metadata = oidc.ClientMetadata{ - RedirectURLs: make([]url.URL, len(c.RedirectURLs)), + RedirectURIs: make([]url.URL, len(c.RedirectURLs)), } for i, us := range c.RedirectURLs { @@ -180,7 +180,7 @@ func (ci *clientIdentity) UnmarshalJSON(data []byte) error { if err != nil { return err } - ci.Metadata.RedirectURLs[i] = *up + ci.Metadata.RedirectURIs[i] = *up } return nil diff --git a/client/client_test.go b/client/client_test.go index d018b466..666e1f05 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -18,7 +18,7 @@ func TestMemClientIdentityRepoNew(t *testing.T) { { id: "foo", meta: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{ Scheme: "https", Host: "example.com", @@ -29,7 +29,7 @@ func TestMemClientIdentityRepoNew(t *testing.T) { { id: "bar", meta: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "https", Host: "example.com/foo"}, url.URL{Scheme: "https", Host: "example.com/bar"}, }, @@ -60,8 +60,8 @@ func TestMemClientIdentityRepoNew(t *testing.T) { t.Errorf("case %d: expected repo to contain newly created Client", i) } - wantURLs := tt.meta.RedirectURLs - gotURLs := all[0].Metadata.RedirectURLs + wantURLs := tt.meta.RedirectURIs + gotURLs := all[0].Metadata.RedirectURIs if !reflect.DeepEqual(wantURLs, gotURLs) { t.Errorf("case %d: redirect url mismatch, want=%v, got=%v", i, wantURLs, gotURLs) } @@ -72,7 +72,7 @@ func TestMemClientIdentityRepoNewDuplicate(t *testing.T) { cr := NewClientIdentityRepo(nil) meta1 := oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "https", Host: "foo.example.com"}, }, } @@ -82,7 +82,7 @@ func TestMemClientIdentityRepoNewDuplicate(t *testing.T) { } meta2 := oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "https", Host: "bar.example.com"}, }, } @@ -174,7 +174,7 @@ func TestClientIdentityUnmarshalJSON(t *testing.T) { sort.Strings(expectedURLs) actualURLs := make([]string, 0) - for _, u := range actual.Metadata.RedirectURLs { + for _, u := range actual.Metadata.RedirectURIs { actualURLs = append(actualURLs, u.String()) } sort.Strings(actualURLs) diff --git a/cmd/dexctl/command_client.go b/cmd/dexctl/command_client.go index 4460b9ad..ee67250d 100644 --- a/cmd/dexctl/command_client.go +++ b/cmd/dexctl/command_client.go @@ -37,7 +37,7 @@ func runNewClient(cmd *cobra.Command, args []string) int { redirectURLs[i] = *u } - cc, err := getDriver().NewClient(oidc.ClientMetadata{RedirectURLs: redirectURLs}) + cc, err := getDriver().NewClient(oidc.ClientMetadata{RedirectURIs: redirectURLs}) if err != nil { stderr("Failed creating new client: %v", err) return 1 diff --git a/cmd/dexctl/driver_api.go b/cmd/dexctl/driver_api.go index b287e5bd..ef05bdee 100644 --- a/cmd/dexctl/driver_api.go +++ b/cmd/dexctl/driver_api.go @@ -21,13 +21,13 @@ func newAPIDriver(pcfg oidc.ProviderConfig, creds oidc.ClientCredentials) (drive trans := &oidc.AuthenticatedTransport{ TokenRefresher: &oidc.ClientCredsTokenRefresher{ - Issuer: pcfg.Issuer, + Issuer: pcfg.Issuer.String(), OIDCClient: oc, }, RoundTripper: http.DefaultTransport, } hc := &http.Client{Transport: trans} - svc, err := schema.NewWithBasePath(hc, pcfg.Issuer) + svc, err := schema.NewWithBasePath(hc, pcfg.Issuer.String()) if err != nil { return nil, err } @@ -41,10 +41,10 @@ type apiDriver struct { func (d *apiDriver) NewClient(meta oidc.ClientMetadata) (*oidc.ClientCredentials, error) { sc := &schema.Client{ - RedirectURIs: make([]string, len(meta.RedirectURLs)), + RedirectURIs: make([]string, len(meta.RedirectURIs)), } - for i, u := range meta.RedirectURLs { + for i, u := range meta.RedirectURIs { sc.RedirectURIs[i] = u.String() } diff --git a/cmd/dexctl/driver_db.go b/cmd/dexctl/driver_db.go index e5c8d760..7f61092a 100644 --- a/cmd/dexctl/driver_db.go +++ b/cmd/dexctl/driver_db.go @@ -31,7 +31,7 @@ func (d *dbDriver) NewClient(meta oidc.ClientMetadata) (*oidc.ClientCredentials, return nil, err } - clientID, err := oidc.GenClientID(meta.RedirectURLs[0].Host) + clientID, err := oidc.GenClientID(meta.RedirectURIs[0].Host) if err != nil { return nil, err } diff --git a/connector/connector_oidc_test.go b/connector/connector_oidc_test.go index df9077ce..4ef99e17 100644 --- a/connector/connector_oidc_test.go +++ b/connector/connector_oidc_test.go @@ -89,8 +89,8 @@ func TestLoginURL(t *testing.T) { Credentials: oidc.ClientCredentials{ID: tt.cid, Secret: "fake-client-secret"}, RedirectURL: tt.redir, ProviderConfig: oidc.ProviderConfig{ - AuthEndpoint: "http://example.com/authorize", - TokenEndpoint: "http://example.com/token", + AuthEndpoint: &url.URL{Scheme: "http", Host: "example.com", Path: "/authorize"}, + TokenEndpoint: &url.URL{Scheme: "http", Host: "example.com", Path: "/token"}, }, Scope: tt.scope, } diff --git a/functional/db_test.go b/functional/db_test.go index 59d950fa..d968a96b 100644 --- a/functional/db_test.go +++ b/functional/db_test.go @@ -193,7 +193,7 @@ func TestDBClientIdentityRepoMetadata(t *testing.T) { r := db.NewClientIdentityRepo(connect(t)) cm := oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "127.0.0.1:5556", Path: "/cb"}, url.URL{Scheme: "https", Host: "example.com", Path: "/callback"}, }, @@ -230,7 +230,7 @@ func TestDBClientIdentityRepoNewDuplicate(t *testing.T) { r := db.NewClientIdentityRepo(connect(t)) meta1 := oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "foo.example.com"}, }, } @@ -240,7 +240,7 @@ func TestDBClientIdentityRepoNewDuplicate(t *testing.T) { } meta2 := oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "bar.example.com"}, }, } @@ -254,7 +254,7 @@ func TestDBClientIdentityRepoAuthenticate(t *testing.T) { r := db.NewClientIdentityRepo(connect(t)) cm := oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "127.0.0.1:5556", Path: "/cb"}, }, } @@ -302,7 +302,7 @@ func TestDBClientIdentityAll(t *testing.T) { r := db.NewClientIdentityRepo(connect(t)) cm := oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "127.0.0.1:5556", Path: "/cb"}, }, } @@ -326,7 +326,7 @@ func TestDBClientIdentityAll(t *testing.T) { } cm = oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "foo.com", Path: "/cb"}, }, } diff --git a/functional/repo/client_repo_test.go b/functional/repo/client_repo_test.go index 81e73bea..b76f6536 100644 --- a/functional/repo/client_repo_test.go +++ b/functional/repo/client_repo_test.go @@ -22,7 +22,7 @@ var ( Secret: "secret-1", }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{ Scheme: "https", Host: "client1.example.com/callback", @@ -36,7 +36,7 @@ var ( Secret: "secret-2", }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{ Scheme: "https", Host: "client2.example.com/callback", diff --git a/integration/client_api_test.go b/integration/client_api_test.go index f36323ad..46a9b376 100644 --- a/integration/client_api_test.go +++ b/integration/client_api_test.go @@ -72,8 +72,8 @@ func TestClientCreate(t *testing.T) { t.Error("Expected new client to exist in repo") } - gotURLs := make([]string, len(meta.RedirectURLs)) - for i, u := range meta.RedirectURLs { + gotURLs := make([]string, len(meta.RedirectURIs)) + for i, u := range meta.RedirectURIs { gotURLs[i] = u.String() } if !reflect.DeepEqual(newClientInput.RedirectURIs, gotURLs) { diff --git a/integration/user_api_test.go b/integration/user_api_test.go index 0b4c1431..6e7eaeaf 100644 --- a/integration/user_api_test.go +++ b/integration/user_api_test.go @@ -104,7 +104,7 @@ func makeUserAPITestFixtures() *userAPITestFixtures { Secret: testClientSecret, }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ testRedirectURL, }, }, @@ -115,7 +115,7 @@ func makeUserAPITestFixtures() *userAPITestFixtures { Secret: "secret", }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ testRedirectURL, }, }, diff --git a/schema/workerschema/mapper.go b/schema/workerschema/mapper.go index 6236f59b..ab9c16e7 100644 --- a/schema/workerschema/mapper.go +++ b/schema/workerschema/mapper.go @@ -13,7 +13,7 @@ func MapSchemaClientToClientIdentity(sc Client) (oidc.ClientIdentity, error) { ID: sc.Id, }, Metadata: oidc.ClientMetadata{ - RedirectURLs: make([]url.URL, len(sc.RedirectURIs)), + RedirectURIs: make([]url.URL, len(sc.RedirectURIs)), }, } @@ -27,7 +27,7 @@ func MapSchemaClientToClientIdentity(sc Client) (oidc.ClientIdentity, error) { return oidc.ClientIdentity{}, errors.New("redirect URL invalid") } - ci.Metadata.RedirectURLs[i] = *u + ci.Metadata.RedirectURIs[i] = *u } return ci, nil @@ -36,9 +36,9 @@ func MapSchemaClientToClientIdentity(sc Client) (oidc.ClientIdentity, error) { func MapClientIdentityToSchemaClient(c oidc.ClientIdentity) Client { cl := Client{ Id: c.Credentials.ID, - RedirectURIs: make([]string, len(c.Metadata.RedirectURLs)), + RedirectURIs: make([]string, len(c.Metadata.RedirectURIs)), } - for i, u := range c.Metadata.RedirectURLs { + for i, u := range c.Metadata.RedirectURIs { cl.RedirectURIs[i] = u.String() } return cl @@ -48,9 +48,9 @@ func MapClientIdentityToSchemaClientWithSecret(c oidc.ClientIdentity) ClientWith cl := ClientWithSecret{ Id: c.Credentials.ID, Secret: c.Credentials.Secret, - RedirectURIs: make([]string, len(c.Metadata.RedirectURLs)), + RedirectURIs: make([]string, len(c.Metadata.RedirectURIs)), } - for i, u := range c.Metadata.RedirectURLs { + for i, u := range c.Metadata.RedirectURIs { cl.RedirectURIs[i] = u.String() } return cl diff --git a/server/client_resource.go b/server/client_resource.go index c1df3862..45f7027b 100644 --- a/server/client_resource.go +++ b/server/client_resource.go @@ -89,7 +89,7 @@ func (c *clientResource) create(w http.ResponseWriter, r *http.Request) { return } - clientID, err := oidc.GenClientID(ci.Metadata.RedirectURLs[0].Host) + clientID, err := oidc.GenClientID(ci.Metadata.RedirectURIs[0].Host) if err != nil { log.Errorf("Failed generating ID for new client: %v", err) writeAPIError(w, http.StatusInternalServerError, newAPIError(errorServerError, "unable to generate client ID")) diff --git a/server/client_resource_test.go b/server/client_resource_test.go index a1d4490d..9557966c 100644 --- a/server/client_resource_test.go +++ b/server/client_resource_test.go @@ -89,13 +89,13 @@ func TestCreateInvalidRequest(t *testing.T) { { req: &http.Request{Method: "POST", URL: u, Header: h, Body: makeBody(`{"redirectURIs":["asdf.com"]}`)}, wantCode: http.StatusBadRequest, - wantBody: `{"error":"invalid_client_metadata","error_description":"invalid redirect URL: scheme not http/https"}`, + wantBody: `{"error":"invalid_client_metadata","error_description":"no host for uri field redirect_uris"}`, }, // uri missing host { req: &http.Request{Method: "POST", URL: u, Header: h, Body: makeBody(`{"redirectURIs":["http://"]}`)}, wantCode: http.StatusBadRequest, - wantBody: `{"error":"invalid_client_metadata","error_description":"invalid redirect URL: host empty"}`, + wantBody: `{"error":"invalid_client_metadata","error_description":"no host for uri field redirect_uris"}`, }, } @@ -183,7 +183,7 @@ func TestList(t *testing.T) { oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ID: "foo", Secret: "bar"}, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "example.com"}, }, }, @@ -202,7 +202,7 @@ func TestList(t *testing.T) { oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ID: "foo", Secret: "bar"}, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "example.com"}, }, }, @@ -210,7 +210,7 @@ func TestList(t *testing.T) { oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ID: "biz", Secret: "bang"}, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "https", Host: "example.com", Path: "one/two/three"}, }, }, diff --git a/server/email_verification.go b/server/email_verification.go index fe3e9746..2bc01fa9 100644 --- a/server/email_verification.go +++ b/server/email_verification.go @@ -158,7 +158,7 @@ func handleVerifyEmailResendFunc( return } - *redirectURL, err = client.ValidRedirectURL(redirectURL, cm.RedirectURLs) + *redirectURL, err = client.ValidRedirectURL(redirectURL, cm.RedirectURIs) if err != nil { switch err { case (client.ErrorInvalidRedirectURL): diff --git a/server/http.go b/server/http.go index 049ae778..c2855269 100644 --- a/server/http.go +++ b/server/http.go @@ -55,7 +55,7 @@ func handleDiscoveryFunc(cfg oidc.ProviderConfig) http.HandlerFunc { return } - b, err := json.Marshal(cfg) + b, err := json.Marshal(&cfg) if err != nil { log.Errorf("Unable to marshal %#v to JSON: %v", cfg, err) } @@ -309,13 +309,13 @@ func handleAuthFunc(srv OIDCServer, idpcs []connector.Connector, tpl *template.T return } - if len(cm.RedirectURLs) == 0 { + if len(cm.RedirectURIs) == 0 { log.Errorf("Client %q has no redirect URLs", acr.ClientID) writeAuthError(w, oauth2.NewError(oauth2.ErrorServerError), acr.State) return } - redirectURL, err := client.ValidRedirectURL(acr.RedirectURL, cm.RedirectURLs) + redirectURL, err := client.ValidRedirectURL(acr.RedirectURL, cm.RedirectURIs) if err != nil { switch err { case (client.ErrorCantChooseRedirectURL): diff --git a/server/http_test.go b/server/http_test.go index 0820a7ce..da35bd83 100644 --- a/server/http_test.go +++ b/server/http_test.go @@ -83,7 +83,7 @@ func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) { Secret: "secrete", }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "client.example.com", Path: "/callback"}, }, }, @@ -206,7 +206,7 @@ func TestHandleAuthFuncResponsesMultipleRedirectURLs(t *testing.T) { Secret: "secrete", }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "foo.example.com", Path: "/callback"}, url.URL{Scheme: "http", Host: "bar.example.com", Path: "/callback"}, }, @@ -363,17 +363,22 @@ func TestHandleDiscoveryFuncMethodNotAllowed(t *testing.T) { } func TestHandleDiscoveryFunc(t *testing.T) { - u := "http://server.example.com" + u := url.URL{Scheme: "http", Host: "server.example.com"} + pathURL := func(path string) *url.URL { + ucopy := u + ucopy.Path = path + return &ucopy + } cfg := oidc.ProviderConfig{ - Issuer: u, - AuthEndpoint: u + httpPathAuth, - TokenEndpoint: u + httpPathToken, - KeysEndpoint: u + httpPathKeys, + Issuer: &u, + AuthEndpoint: pathURL(httpPathAuth), + TokenEndpoint: pathURL(httpPathToken), + KeysEndpoint: pathURL(httpPathKeys), GrantTypesSupported: []string{oauth2.GrantTypeAuthCode}, ResponseTypesSupported: []string{"code"}, SubjectTypesSupported: []string{"public"}, - IDTokenAlgValuesSupported: []string{"RS256"}, + IDTokenSigningAlgValues: []string{"RS256"}, TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, } diff --git a/server/password.go b/server/password.go index f2f9ffe6..4d517f05 100644 --- a/server/password.go +++ b/server/password.go @@ -134,7 +134,7 @@ func (h *SendResetPasswordEmailHandler) validateRedirectURL(clientID string, red return url.URL{}, false } - validURL, err := client.ValidRedirectURL(parsed, cm.RedirectURLs) + validURL, err := client.ValidRedirectURL(parsed, cm.RedirectURIs) if err != nil { log.Errorf("Invalid redirectURL for clientID: redirectURL:%q, clientID:%q", redirectURL, clientID) return url.URL{}, false diff --git a/server/server.go b/server/server.go index f2da3739..b8206397 100644 --- a/server/server.go +++ b/server/server.go @@ -110,19 +110,24 @@ func (s *Server) KillSession(sessionKey string) error { return err } -func (s *Server) ProviderConfig() oidc.ProviderConfig { - iss := s.IssuerURL.String() - cfg := oidc.ProviderConfig{ - Issuer: iss, +func (s *Server) pathURL(path string) *url.URL { + u := s.IssuerURL + u.Path = path + return &u +} - AuthEndpoint: iss + httpPathAuth, - TokenEndpoint: iss + httpPathToken, - KeysEndpoint: iss + httpPathKeys, +func (s *Server) ProviderConfig() oidc.ProviderConfig { + cfg := oidc.ProviderConfig{ + Issuer: &s.IssuerURL, + + AuthEndpoint: s.pathURL(httpPathAuth), + TokenEndpoint: s.pathURL(httpPathToken), + KeysEndpoint: s.pathURL(httpPathKeys), GrantTypesSupported: []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeClientCreds}, ResponseTypesSupported: []string{"code"}, SubjectTypesSupported: []string{"public"}, - IDTokenAlgValuesSupported: []string{"RS256"}, + IDTokenSigningAlgValues: []string{"RS256"}, TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, } diff --git a/server/server_test.go b/server/server_test.go index 5a162c16..65e0162f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -17,6 +17,7 @@ import ( "github.com/coreos/go-oidc/key" "github.com/coreos/go-oidc/oauth2" "github.com/coreos/go-oidc/oidc" + "github.com/kylelemons/godebug/pretty" ) type StaticKeyManager struct { @@ -100,20 +101,21 @@ func TestServerProviderConfig(t *testing.T) { srv := &Server{IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}} want := oidc.ProviderConfig{ - Issuer: "http://server.example.com", - AuthEndpoint: "http://server.example.com/auth", - TokenEndpoint: "http://server.example.com/token", - KeysEndpoint: "http://server.example.com/keys", + Issuer: &url.URL{Scheme: "http", Host: "server.example.com"}, + AuthEndpoint: &url.URL{Scheme: "http", Host: "server.example.com", Path: "/auth"}, + TokenEndpoint: &url.URL{Scheme: "http", Host: "server.example.com", Path: "/token"}, + KeysEndpoint: &url.URL{Scheme: "http", Host: "server.example.com", Path: "/keys"}, + GrantTypesSupported: []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeClientCreds}, ResponseTypesSupported: []string{"code"}, SubjectTypesSupported: []string{"public"}, - IDTokenAlgValuesSupported: []string{"RS256"}, + IDTokenSigningAlgValues: []string{"RS256"}, TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, } got := srv.ProviderConfig() - if !reflect.DeepEqual(want, got) { - t.Fatalf("want=%#v, got=%#v", want, got) + if diff := pretty.Compare(want, got); diff != "" { + t.Fatalf("provider config did not match expected: %s", diff) } } @@ -131,7 +133,7 @@ func TestServerNewSession(t *testing.T) { Secret: "secrete", }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{ Scheme: "http", Host: "client.example.com", @@ -141,7 +143,7 @@ func TestServerNewSession(t *testing.T) { }, } - key, err := srv.NewSession("bogus_idpc", ci.Credentials.ID, state, ci.Metadata.RedirectURLs[0], nonce, false, []string{"openid"}) + key, err := srv.NewSession("bogus_idpc", ci.Credentials.ID, state, ci.Metadata.RedirectURIs[0], nonce, false, []string{"openid"}) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -156,8 +158,8 @@ func TestServerNewSession(t *testing.T) { t.Fatalf("Unable to add Identity to Session: %v", err) } - if !reflect.DeepEqual(ci.Metadata.RedirectURLs[0], ses.RedirectURL) { - t.Fatalf("Session created with incorrect RedirectURL: want=%#v got=%#v", ci.Metadata.RedirectURLs[0], ses.RedirectURL) + if !reflect.DeepEqual(ci.Metadata.RedirectURIs[0], ses.RedirectURL) { + t.Fatalf("Session created with incorrect RedirectURL: want=%#v got=%#v", ci.Metadata.RedirectURIs[0], ses.RedirectURL) } if ci.Credentials.ID != ses.ClientID { @@ -180,7 +182,7 @@ func TestServerLogin(t *testing.T) { Secret: "secrete", }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{ Scheme: "http", Host: "client.example.com", @@ -197,7 +199,7 @@ func TestServerLogin(t *testing.T) { sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) sm.GenerateCode = staticGenerateCodeFunc("fakecode") - sessionID, err := sm.NewSession("test_connector_id", ci.Credentials.ID, "bogus", ci.Metadata.RedirectURLs[0], "", false, []string{"openid"}) + sessionID, err := sm.NewSession("test_connector_id", ci.Credentials.ID, "bogus", ci.Metadata.RedirectURIs[0], "", false, []string{"openid"}) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -269,7 +271,7 @@ func TestServerLoginDisabledUser(t *testing.T) { Secret: "secrete", }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ url.URL{ Scheme: "http", Host: "client.example.com", @@ -286,7 +288,7 @@ func TestServerLoginDisabledUser(t *testing.T) { sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) sm.GenerateCode = staticGenerateCodeFunc("fakecode") - sessionID, err := sm.NewSession("test_connector_id", ci.Credentials.ID, "bogus", ci.Metadata.RedirectURLs[0], "", false, []string{"openid"}) + sessionID, err := sm.NewSession("test_connector_id", ci.Credentials.ID, "bogus", ci.Metadata.RedirectURIs[0], "", false, []string{"openid"}) if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/server/testutil.go b/server/testutil.go index fed05f25..b3770121 100644 --- a/server/testutil.go +++ b/server/testutil.go @@ -133,7 +133,7 @@ func makeTestFixtures() (*testFixtures, error) { Secret: testClientSecret, }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ testRedirectURL, }, }, diff --git a/user/api/api.go b/user/api/api.go index 2eea7f5c..0c5f5eac 100644 --- a/user/api/api.go +++ b/user/api/api.go @@ -153,7 +153,7 @@ func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (s return schema.UserCreateResponse{}, mapError(err) } - validRedirURL, err := client.ValidRedirectURL(&redirURL, metadata.RedirectURLs) + validRedirURL, err := client.ValidRedirectURL(&redirURL, metadata.RedirectURIs) if err != nil { return schema.UserCreateResponse{}, ErrorInvalidRedirectURL } diff --git a/user/api/api_test.go b/user/api/api_test.go index 967db2ac..404fa9d8 100644 --- a/user/api/api_test.go +++ b/user/api/api_test.go @@ -136,7 +136,7 @@ func makeTestFixtures() (*UsersAPI, *testEmailer) { Secret: "secrete", }, Metadata: oidc.ClientMetadata{ - RedirectURLs: []url.URL{ + RedirectURIs: []url.URL{ validRedirURL, }, }, From 9796a1e648a409ae79f7dd930fddb072e1cc27de Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Tue, 12 Jan 2016 17:17:50 -0800 Subject: [PATCH 3/4] *: add migration to update JSON fields and require postgres 9.4+ The "redirectURLs" field in the client metadata has been updated to the correct "redirect_uris". To allow backwards compatibility use Postgres' JSON features to update the actual JSON in the text field. json_build_object was introduced in Postgres 9.4. So update the documentations to require at least this version. --- Documentation/getting-started.md | 2 +- README.md | 2 +- db/client.go | 45 +---------- db/migrate_test.go | 81 ++++++++++++++++++- .../0010_client_metadata_field_changed.sql | 9 +++ db/migrations/assets.go | 77 +++++++++++------- 6 files changed, 143 insertions(+), 73 deletions(-) create mode 100644 db/migrations/0010_client_metadata_field_changed.sql diff --git a/Documentation/getting-started.md b/Documentation/getting-started.md index 679959ac..b5c812aa 100644 --- a/Documentation/getting-started.md +++ b/Documentation/getting-started.md @@ -14,7 +14,7 @@ We'll also start the example web app, so we can try registering and logging in. Before continuing, you must have the following installed on your system: * Go 1.4 or greater -* Postgres 9.0 or greater (this guide also assumes that Postgres is up and running) +* Postgres 9.4 or greater (this guide also assumes that Postgres is up and running) In addition, if you wish to try out authenticating against Google's OIDC backend, you must have a new client registered with Google: diff --git a/README.md b/README.md index 8b871908..27ccf988 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ dex consists of multiple components: - configure identity provider connectors - administer OIDC client identities - **database**; a database is used to for persistent storage for keys, users, - OAuth sessions and other data. Currently Postgres is the only supported + OAuth sessions and other data. Currently Postgres (9.4+) is the only supported database. A typical dex deployment consists of N dex-workers behind a load balanacer, and one dex-overlord. diff --git a/db/client.go b/db/client.go index 89a1667f..2912783b 100644 --- a/db/client.go +++ b/db/client.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "net/url" "reflect" "github.com/coreos/go-oidc/oidc" @@ -49,7 +48,7 @@ func newClientIdentityModel(id string, secret []byte, meta *oidc.ClientMetadata) return nil, err } - bmeta, err := json.Marshal(newClientMetadataJSON(meta)) + bmeta, err := json.Marshal(meta) if err != nil { return nil, err } @@ -70,38 +69,6 @@ type clientIdentityModel struct { DexAdmin bool `db:"dex_admin"` } -func newClientMetadataJSON(cm *oidc.ClientMetadata) *clientMetadataJSON { - cmj := clientMetadataJSON{ - RedirectURLs: make([]string, len(cm.RedirectURLs)), - } - - for i, u := range cm.RedirectURLs { - cmj.RedirectURLs[i] = (&u).String() - } - - return &cmj -} - -type clientMetadataJSON struct { - RedirectURLs []string `json:"redirectURLs"` -} - -func (cmj clientMetadataJSON) ClientMetadata() (*oidc.ClientMetadata, error) { - cm := oidc.ClientMetadata{ - RedirectURLs: make([]url.URL, len(cmj.RedirectURLs)), - } - - for i, us := range cmj.RedirectURLs { - up, err := url.Parse(us) - if err != nil { - return nil, err - } - cm.RedirectURLs[i] = *up - } - - return &cm, nil -} - func (m *clientIdentityModel) ClientIdentity() (*oidc.ClientIdentity, error) { ci := oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ @@ -110,18 +77,10 @@ func (m *clientIdentityModel) ClientIdentity() (*oidc.ClientIdentity, error) { }, } - var cmj clientMetadataJSON - err := json.Unmarshal([]byte(m.Metadata), &cmj) - if err != nil { + if err := json.Unmarshal([]byte(m.Metadata), &ci.Metadata); err != nil { return nil, err } - cm, err := cmj.ClientMetadata() - if err != nil { - return nil, err - } - - ci.Metadata = *cm return &ci, nil } diff --git a/db/migrate_test.go b/db/migrate_test.go index 41cbd74f..25c08968 100644 --- a/db/migrate_test.go +++ b/db/migrate_test.go @@ -3,6 +3,7 @@ package db import ( "fmt" "os" + "strconv" "testing" "github.com/go-gorp/gorp" @@ -25,7 +26,7 @@ func initDB(dsn string) *gorp.DbMap { func TestGetPlannedMigrations(t *testing.T) { dsn := os.Getenv("DEX_TEST_DSN") if dsn == "" { - t.Logf("Test will not run without DEX_TEST_DSN environment variable.") + t.Skip("Test will not run without DEX_TEST_DSN environment variable.") return } dbMap := initDB(dsn) @@ -40,3 +41,81 @@ func TestGetPlannedMigrations(t *testing.T) { t.Fatalf("expected non-empty migrations") } } + +func TestMigrateClientMetadata(t *testing.T) { + dsn := os.Getenv("DEX_TEST_DSN") + if dsn == "" { + t.Skip("Test will not run without DEX_TEST_DSN environment variable.") + return + } + dbMap := initDB(dsn) + + nMigrations := 9 + n, err := MigrateMaxMigrations(dbMap, nMigrations) + if err != nil { + t.Fatalf("failed to perform initial migration: %v", err) + } + if n != nMigrations { + t.Fatalf("expected to perform %d migrations, got %d", nMigrations, n) + } + + tests := []struct { + before string + after string + }{ + // only update rows without a "redirect_uris" key + { + `{"redirectURLs":["foo"]}`, + `{"redirectURLs" : ["foo"], "redirect_uris" : ["foo"]}`, + }, + { + `{"redirectURLs":["foo","bar"]}`, + `{"redirectURLs" : ["foo","bar"], "redirect_uris" : ["foo","bar"]}`, + }, + { + `{"redirect_uris":["foo"],"another_field":8}`, + `{"redirect_uris":["foo"],"another_field":8}`, + }, + { + `{"redirectURLs" : ["foo"], "redirect_uris" : ["foo"]}`, + `{"redirectURLs" : ["foo"], "redirect_uris" : ["foo"]}`, + }, + } + + for i, tt := range tests { + model := &clientIdentityModel{ + ID: strconv.Itoa(i), + Secret: []byte("verysecret"), + Metadata: tt.before, + } + if err := dbMap.Insert(model); err != nil { + t.Fatalf("could not insert model: %v", err) + } + } + + n, err = MigrateMaxMigrations(dbMap, 1) + if err != nil { + t.Fatalf("failed to perform initial migration: %v", err) + } + if n != 1 { + t.Fatalf("expected to perform 1 migration, got %d", n) + } + + for i, tt := range tests { + id := strconv.Itoa(i) + m, err := dbMap.Get(clientIdentityModel{}, id) + if err != nil { + t.Errorf("case %d: failed to get model: %err", i, err) + continue + } + cim, ok := m.(*clientIdentityModel) + if !ok { + t.Errorf("case %d: unrecognized model type: %T", i, m) + continue + } + + if cim.Metadata != tt.after { + t.Errorf("case %d: want=%q, got=%q", i, tt.after, cim.Metadata) + } + } +} diff --git a/db/migrations/0010_client_metadata_field_changed.sql b/db/migrations/0010_client_metadata_field_changed.sql new file mode 100644 index 00000000..a561ae39 --- /dev/null +++ b/db/migrations/0010_client_metadata_field_changed.sql @@ -0,0 +1,9 @@ +-- +migrate Up +UPDATE client_identity +SET metadata = text( + json_build_object( + 'redirectURLs', json(json(metadata)->>'redirectURLs'), + 'redirect_uris', json(json(metadata)->>'redirectURLs') + ) + ) +WHERE (json(metadata)->>'redirect_uris') IS NULL; diff --git a/db/migrations/assets.go b/db/migrations/assets.go index 7da1604b..13b7c535 100644 --- a/db/migrations/assets.go +++ b/db/migrations/assets.go @@ -9,6 +9,7 @@ // 0007_session_scope.sql // 0008_users_active_or_inactive.sql // 0009_key_not_primary_key.sql +// 0010_client_metadata_field_changed.sql // DO NOT EDIT! package migrations @@ -91,7 +92,7 @@ func dbMigrations0001_initial_migrationSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "db/migrations/0001_initial_migration.sql", size: 1388, mode: os.FileMode(420), modTime: time.Unix(1, 0)} + info := bindataFileInfo{name: "db/migrations/0001_initial_migration.sql", size: 1388, mode: os.FileMode(436), modTime: time.Unix(1, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -111,7 +112,7 @@ func dbMigrations0002_dex_adminSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "db/migrations/0002_dex_admin.sql", size: 126, mode: os.FileMode(420), modTime: time.Unix(1, 0)} + info := bindataFileInfo{name: "db/migrations/0002_dex_admin.sql", size: 126, mode: os.FileMode(436), modTime: time.Unix(1, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -131,7 +132,7 @@ func dbMigrations0003_user_created_atSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "db/migrations/0003_user_created_at.sql", size: 111, mode: os.FileMode(420), modTime: time.Unix(1, 0)} + info := bindataFileInfo{name: "db/migrations/0003_user_created_at.sql", size: 111, mode: os.FileMode(436), modTime: time.Unix(1, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -151,7 +152,7 @@ func dbMigrations0004_session_nonceSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "db/migrations/0004_session_nonce.sql", size: 60, mode: os.FileMode(420), modTime: time.Unix(1, 0)} + info := bindataFileInfo{name: "db/migrations/0004_session_nonce.sql", size: 60, mode: os.FileMode(436), modTime: time.Unix(1, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -171,7 +172,7 @@ func dbMigrations0005_refresh_token_createSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "db/migrations/0005_refresh_token_create.sql", size: 506, mode: os.FileMode(420), modTime: time.Unix(1, 0)} + info := bindataFileInfo{name: "db/migrations/0005_refresh_token_create.sql", size: 506, mode: os.FileMode(436), modTime: time.Unix(1, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -191,7 +192,7 @@ func dbMigrations0006_user_email_uniqueSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "db/migrations/0006_user_email_unique.sql", size: 99, mode: os.FileMode(420), modTime: time.Unix(1, 0)} + info := bindataFileInfo{name: "db/migrations/0006_user_email_unique.sql", size: 99, mode: os.FileMode(436), modTime: time.Unix(1, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -211,7 +212,7 @@ func dbMigrations0007_session_scopeSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "db/migrations/0007_session_scope.sql", size: 60, mode: os.FileMode(420), modTime: time.Unix(1, 0)} + info := bindataFileInfo{name: "db/migrations/0007_session_scope.sql", size: 60, mode: os.FileMode(436), modTime: time.Unix(1, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -231,7 +232,7 @@ func dbMigrations0008_users_active_or_inactiveSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "db/migrations/0008_users_active_or_inactive.sql", size: 110, mode: os.FileMode(420), modTime: time.Unix(1, 0)} + info := bindataFileInfo{name: "db/migrations/0008_users_active_or_inactive.sql", size: 110, mode: os.FileMode(436), modTime: time.Unix(1, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -251,7 +252,27 @@ func dbMigrations0009_key_not_primary_keySql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "db/migrations/0009_key_not_primary_key.sql", size: 182, mode: os.FileMode(420), modTime: time.Unix(1, 0)} + info := bindataFileInfo{name: "db/migrations/0009_key_not_primary_key.sql", size: 182, mode: os.FileMode(436), modTime: time.Unix(1, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _dbMigrations0010_client_metadata_field_changedSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd2\xd5\x55\xd0\xce\xcd\x4c\x2f\x4a\x2c\x49\x55\x08\x2d\xe0\x0a\x0d\x70\x71\x0c\x71\x55\x48\xce\xc9\x4c\xcd\x2b\x89\xcf\x4c\x01\x92\x99\x25\x95\x5c\xc1\xae\x21\x0a\xb9\xa9\x25\x89\x29\x89\x25\x89\x0a\xb6\x0a\x25\xa9\x15\x25\x1a\x5c\x0a\x40\x90\x55\x9c\x9f\x17\x9f\x54\x9a\x99\x93\x12\x9f\x9f\x94\x95\x9a\x0c\x15\x06\x01\xf5\xa2\xd4\x94\xcc\x22\xa0\x50\x68\x90\x4f\xb1\xba\x0e\x58\xa9\x06\x98\x80\x99\xa4\xa9\x6b\x67\x87\xaa\x4a\x53\x07\x53\x7b\x7c\x69\x51\x26\xd1\xfa\xc1\xda\x81\xa4\x26\x57\xb8\x87\x6b\x90\xab\x02\x1e\x0d\x10\x73\x35\x15\x3c\x83\x15\xfc\x42\x7d\x7c\xac\xb9\x00\x01\x00\x00\xff\xff\xeb\xe6\x9a\x19\x0b\x01\x00\x00") + +func dbMigrations0010_client_metadata_field_changedSqlBytes() ([]byte, error) { + return bindataRead( + _dbMigrations0010_client_metadata_field_changedSql, + "db/migrations/0010_client_metadata_field_changed.sql", + ) +} + +func dbMigrations0010_client_metadata_field_changedSql() (*asset, error) { + bytes, err := dbMigrations0010_client_metadata_field_changedSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "db/migrations/0010_client_metadata_field_changed.sql", size: 267, mode: os.FileMode(436), modTime: time.Unix(1, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -308,15 +329,16 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ - "db/migrations/0001_initial_migration.sql": dbMigrations0001_initial_migrationSql, - "db/migrations/0002_dex_admin.sql": dbMigrations0002_dex_adminSql, - "db/migrations/0003_user_created_at.sql": dbMigrations0003_user_created_atSql, - "db/migrations/0004_session_nonce.sql": dbMigrations0004_session_nonceSql, - "db/migrations/0005_refresh_token_create.sql": dbMigrations0005_refresh_token_createSql, - "db/migrations/0006_user_email_unique.sql": dbMigrations0006_user_email_uniqueSql, - "db/migrations/0007_session_scope.sql": dbMigrations0007_session_scopeSql, - "db/migrations/0008_users_active_or_inactive.sql": dbMigrations0008_users_active_or_inactiveSql, - "db/migrations/0009_key_not_primary_key.sql": dbMigrations0009_key_not_primary_keySql, + "db/migrations/0001_initial_migration.sql": dbMigrations0001_initial_migrationSql, + "db/migrations/0002_dex_admin.sql": dbMigrations0002_dex_adminSql, + "db/migrations/0003_user_created_at.sql": dbMigrations0003_user_created_atSql, + "db/migrations/0004_session_nonce.sql": dbMigrations0004_session_nonceSql, + "db/migrations/0005_refresh_token_create.sql": dbMigrations0005_refresh_token_createSql, + "db/migrations/0006_user_email_unique.sql": dbMigrations0006_user_email_uniqueSql, + "db/migrations/0007_session_scope.sql": dbMigrations0007_session_scopeSql, + "db/migrations/0008_users_active_or_inactive.sql": dbMigrations0008_users_active_or_inactiveSql, + "db/migrations/0009_key_not_primary_key.sql": dbMigrations0009_key_not_primary_keySql, + "db/migrations/0010_client_metadata_field_changed.sql": dbMigrations0010_client_metadata_field_changedSql, } // AssetDir returns the file names below a certain @@ -362,15 +384,16 @@ type bintree struct { var _bintree = &bintree{nil, map[string]*bintree{ "db": &bintree{nil, map[string]*bintree{ "migrations": &bintree{nil, map[string]*bintree{ - "0001_initial_migration.sql": &bintree{dbMigrations0001_initial_migrationSql, map[string]*bintree{}}, - "0002_dex_admin.sql": &bintree{dbMigrations0002_dex_adminSql, map[string]*bintree{}}, - "0003_user_created_at.sql": &bintree{dbMigrations0003_user_created_atSql, map[string]*bintree{}}, - "0004_session_nonce.sql": &bintree{dbMigrations0004_session_nonceSql, map[string]*bintree{}}, - "0005_refresh_token_create.sql": &bintree{dbMigrations0005_refresh_token_createSql, map[string]*bintree{}}, - "0006_user_email_unique.sql": &bintree{dbMigrations0006_user_email_uniqueSql, map[string]*bintree{}}, - "0007_session_scope.sql": &bintree{dbMigrations0007_session_scopeSql, map[string]*bintree{}}, - "0008_users_active_or_inactive.sql": &bintree{dbMigrations0008_users_active_or_inactiveSql, map[string]*bintree{}}, - "0009_key_not_primary_key.sql": &bintree{dbMigrations0009_key_not_primary_keySql, map[string]*bintree{}}, + "0001_initial_migration.sql": &bintree{dbMigrations0001_initial_migrationSql, map[string]*bintree{}}, + "0002_dex_admin.sql": &bintree{dbMigrations0002_dex_adminSql, map[string]*bintree{}}, + "0003_user_created_at.sql": &bintree{dbMigrations0003_user_created_atSql, map[string]*bintree{}}, + "0004_session_nonce.sql": &bintree{dbMigrations0004_session_nonceSql, map[string]*bintree{}}, + "0005_refresh_token_create.sql": &bintree{dbMigrations0005_refresh_token_createSql, map[string]*bintree{}}, + "0006_user_email_unique.sql": &bintree{dbMigrations0006_user_email_uniqueSql, map[string]*bintree{}}, + "0007_session_scope.sql": &bintree{dbMigrations0007_session_scopeSql, map[string]*bintree{}}, + "0008_users_active_or_inactive.sql": &bintree{dbMigrations0008_users_active_or_inactiveSql, map[string]*bintree{}}, + "0009_key_not_primary_key.sql": &bintree{dbMigrations0009_key_not_primary_keySql, map[string]*bintree{}}, + "0010_client_metadata_field_changed.sql": &bintree{dbMigrations0010_client_metadata_field_changedSql, map[string]*bintree{}}, }}, }}, }} From 04cd1851aa0cbdc0aeda96a59154f9cff8cbaced Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Thu, 21 Jan 2016 16:07:41 -0800 Subject: [PATCH 4/4] server: add dynamic client registration --- cmd/dex-worker/main.go | 18 ++-- server/client_registration.go | 57 ++++++++++ server/client_registration_test.go | 161 +++++++++++++++++++++++++++++ server/config.go | 22 ++-- server/http.go | 27 ++--- server/server.go | 28 +++-- 6 files changed, 271 insertions(+), 42 deletions(-) create mode 100644 server/client_registration.go create mode 100644 server/client_registration_test.go diff --git a/cmd/dex-worker/main.go b/cmd/dex-worker/main.go index ae960ed3..e24491c6 100644 --- a/cmd/dex-worker/main.go +++ b/cmd/dex-worker/main.go @@ -45,6 +45,7 @@ func main() { emailConfig := fs.String("email-cfg", "./static/fixtures/emailer.json", "configures emailer.") enableRegistration := fs.Bool("enable-registration", false, "Allows users to self-register") + enableClientRegistration := fs.Bool("enable-client-registration", false, "Allow dynamic registration of clients") noDB := fs.Bool("no-db", false, "manage entities in-process w/o any encryption, used only for single-node testing") @@ -117,14 +118,15 @@ func main() { } scfg := server.ServerConfig{ - IssuerURL: *issuer, - TemplateDir: *templates, - EmailTemplateDirs: emailTemplateDirs, - EmailFromAddress: *emailFrom, - EmailerConfigFile: *emailConfig, - IssuerName: *issuerName, - IssuerLogoURL: *issuerLogoURL, - EnableRegistration: *enableRegistration, + IssuerURL: *issuer, + TemplateDir: *templates, + EmailTemplateDirs: emailTemplateDirs, + EmailFromAddress: *emailFrom, + EmailerConfigFile: *emailConfig, + IssuerName: *issuerName, + IssuerLogoURL: *issuerLogoURL, + EnableRegistration: *enableRegistration, + EnableClientRegistration: *enableClientRegistration, } if *noDB { diff --git a/server/client_registration.go b/server/client_registration.go new file mode 100644 index 00000000..f53cc90d --- /dev/null +++ b/server/client_registration.go @@ -0,0 +1,57 @@ +package server + +import ( + "encoding/json" + "net/http" + + "github.com/coreos/dex/pkg/log" + "github.com/coreos/go-oidc/oauth2" + "github.com/coreos/go-oidc/oidc" +) + +const ( + invalidRedirectURI = "invalid_redirect_uri" + invalidClientMetadata = "invalid_client_metadata" +) + +func (s *Server) handleClientRegistration(w http.ResponseWriter, r *http.Request) { + resp, err := s.handleClientRegistrationRequest(r) + if err != nil { + code := http.StatusBadRequest + if err.Type == oauth2.ErrorServerError { + code = http.StatusInternalServerError + } + writeResponseWithBody(w, code, err) + } else { + writeResponseWithBody(w, http.StatusCreated, resp) + } +} + +func (s *Server) handleClientRegistrationRequest(r *http.Request) (*oidc.ClientRegistrationResponse, *apiError) { + var clientMetadata oidc.ClientMetadata + if err := json.NewDecoder(r.Body).Decode(&clientMetadata); err != nil { + return nil, newAPIError(oauth2.ErrorInvalidRequest, err.Error()) + } + if err := s.ProviderConfig().Supports(clientMetadata); err != nil { + return nil, newAPIError(invalidClientMetadata, err.Error()) + } + + // metadata is guarenteed to have at least one redirect_uri by earlier validation. + id, err := oidc.GenClientID(clientMetadata.RedirectURIs[0].Host) + if err != nil { + log.Errorf("Faild to create client ID: %v", err) + return nil, newAPIError(oauth2.ErrorServerError, "unable to save client metadata") + } + + creds, err := s.ClientIdentityRepo.New(id, clientMetadata) + if err != nil { + log.Errorf("Failed to create new client identity: %v", err) + return nil, newAPIError(oauth2.ErrorServerError, "unable to save client metadata") + } + + return &oidc.ClientRegistrationResponse{ + ClientID: creds.ID, + ClientSecret: creds.Secret, + ClientMetadata: clientMetadata, + }, nil +} diff --git a/server/client_registration_test.go b/server/client_registration_test.go new file mode 100644 index 00000000..2dc647cc --- /dev/null +++ b/server/client_registration_test.go @@ -0,0 +1,161 @@ +package server + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/coreos/go-oidc/oauth2" + "github.com/coreos/go-oidc/oidc" + "github.com/kylelemons/godebug/pretty" +) + +func TestClientRegistration(t *testing.T) { + tests := []struct { + body string + code int + }{ + {"", http.StatusBadRequest}, + { + `{ + "redirect_uris": [ + "https://client.example.org/callback", + "https://client.example.org/callback2" + ] + }`, + http.StatusCreated, + }, + { + // Requesting unsupported client metadata fields (user_info_encrypted). + `{ + "application_type": "web", + "redirect_uris":[ + "https://client.example.org/callback", + "https://client.example.org/callback2" + ], + "client_name": "My Example", + "logo_uri": "https://client.example.org/logo.png", + "subject_type": "pairwise", + "sector_identifier_uri": "https://other.example.net/file_of_redirect_uris.json", + "token_endpoint_auth_method": "client_secret_basic", + "jwks_uri": "https://client.example.org/my_public_keys.jwks", + "userinfo_encrypted_response_alg": "RSA1_5", + "userinfo_encrypted_response_enc": "A128CBC-HS256", + "contacts": ["ve7jtb@example.org", "mary@example.org"], + "request_uris": [ + "https://client.example.org/rf.txt#qpXaRLh_n93TTR9F252ValdatUQvQiJi5BDub2BeznA" ] + }`, + http.StatusBadRequest, + }, + { + `{ + "application_type": "web", + "redirect_uris":[ + "https://client.example.org/callback", + "https://client.example.org/callback2" + ], + "client_name": "My Example", + "logo_uri": "https://client.example.org/logo.png", + "subject_type": "pairwise", + "sector_identifier_uri": "https://other.example.net/file_of_redirect_uris.json", + "token_endpoint_auth_method": "client_secret_basic", + "jwks_uri": "https://client.example.org/my_public_keys.jwks", + "contacts": ["ve7jtb@example.org", "mary@example.org"], + "request_uris": [ + "https://client.example.org/rf.txt#qpXaRLh_n93TTR9F252ValdatUQvQiJi5BDub2BeznA" ] + }`, + http.StatusCreated, + }, + } + + var handler http.Handler + f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler.ServeHTTP(w, r) + }) + testServer := httptest.NewServer(f) + + issuerURL, err := url.Parse(testServer.URL) + if err != nil { + t.Fatal(err) + } + defer testServer.Close() + + for i, tt := range tests { + fixtures, err := makeTestFixtures() + if err != nil { + t.Fatal(err) + } + fixtures.srv.IssuerURL = *issuerURL + fixtures.srv.EnableClientRegistration = true + + handler = fixtures.srv.HTTPHandler() + + err = func() error { + // GET provider config through discovery URL. + resp, err := http.Get(testServer.URL + "/.well-known/openid-configuration") + if err != nil { + return fmt.Errorf("GET config: %v", err) + } + var cfg oidc.ProviderConfig + err = json.NewDecoder(resp.Body).Decode(&cfg) + resp.Body.Close() + if err != nil { + return fmt.Errorf("decode resp: %v", err) + } + + if cfg.RegistrationEndpoint == nil { + return errors.New("registration endpoint not available") + } + + // POST registration request to registration endpoint. + body := strings.NewReader(tt.body) + resp, err = http.Post(cfg.RegistrationEndpoint.String(), "application/json", body) + if err != nil { + return fmt.Errorf("POSTing client metadata: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != tt.code { + return fmt.Errorf("expected status code=%d, got=%d", tt.code, resp.StatusCode) + } + + if resp.StatusCode != http.StatusCreated { + var oauthErr oauth2.Error + if err := json.NewDecoder(resp.Body).Decode(&oauthErr); err != nil { + return fmt.Errorf("failed to decode oauth2 error: %v", err) + } + if oauthErr.Type == "" { + return fmt.Errorf("got oauth2 error with no 'error' field") + } + return nil + } + + // Read registration response. + var r oidc.ClientRegistrationResponse + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { + return fmt.Errorf("decode response: %v", err) + } + if r.ClientID == "" { + return fmt.Errorf("no client id in registration response") + } + + metadata, err := fixtures.clientIdentityRepo.Metadata(r.ClientID) + if err != nil { + return fmt.Errorf("failed to lookup client id after creation") + } + + if diff := pretty.Compare(&metadata, &r.ClientMetadata); diff != "" { + return fmt.Errorf("metadata in response did not match metadata in db: %s", diff) + } + + return nil + }() + if err != nil { + t.Errorf("case %d: %v", i, err) + } + } +} diff --git a/server/config.go b/server/config.go index f70065a3..dfe1f0b0 100644 --- a/server/config.go +++ b/server/config.go @@ -26,15 +26,16 @@ import ( ) type ServerConfig struct { - IssuerURL string - IssuerName string - IssuerLogoURL string - TemplateDir string - EmailTemplateDirs []string - EmailFromAddress string - EmailerConfigFile string - StateConfig StateConfigurer - EnableRegistration bool + IssuerURL string + IssuerName string + IssuerLogoURL string + TemplateDir string + EmailTemplateDirs []string + EmailFromAddress string + EmailerConfigFile string + StateConfig StateConfigurer + EnableRegistration bool + EnableClientRegistration bool } type StateConfigurer interface { @@ -73,7 +74,8 @@ func (cfg *ServerConfig) Server() (*Server, error) { HealthChecks: []health.Checkable{km}, Connectors: []connector.Connector{}, - EnableRegistration: cfg.EnableRegistration, + EnableRegistration: cfg.EnableRegistration, + EnableClientRegistration: cfg.EnableClientRegistration, } err = cfg.StateConfig.Configure(&srv) diff --git a/server/http.go b/server/http.go index c2855269..19b8f8c6 100644 --- a/server/http.go +++ b/server/http.go @@ -29,19 +29,20 @@ const ( ) var ( - httpPathDiscovery = "/.well-known/openid-configuration" - httpPathToken = "/token" - httpPathKeys = "/keys" - httpPathAuth = "/auth" - httpPathHealth = "/health" - httpPathAPI = "/api" - httpPathRegister = "/register" - httpPathEmailVerify = "/verify-email" - httpPathVerifyEmailResend = "/resend-verify-email" - httpPathSendResetPassword = "/send-reset-password" - httpPathResetPassword = "/reset-password" - httpPathAcceptInvitation = "/accept-invitation" - httpPathDebugVars = "/debug/vars" + httpPathDiscovery = "/.well-known/openid-configuration" + httpPathToken = "/token" + httpPathKeys = "/keys" + httpPathAuth = "/auth" + httpPathHealth = "/health" + httpPathAPI = "/api" + httpPathRegister = "/register" + httpPathEmailVerify = "/verify-email" + httpPathVerifyEmailResend = "/resend-verify-email" + httpPathSendResetPassword = "/send-reset-password" + httpPathResetPassword = "/reset-password" + httpPathAcceptInvitation = "/accept-invitation" + httpPathDebugVars = "/debug/vars" + httpPathClientRegistration = "/registration" cookieLastSeen = "LastSeen" cookieShowEmailVerifiedMessage = "ShowEmailVerifiedMessage" diff --git a/server/server.go b/server/server.go index b8206397..f9a81259 100644 --- a/server/server.go +++ b/server/server.go @@ -74,6 +74,7 @@ type Server struct { RefreshTokenRepo refresh.RefreshTokenRepo UserEmailer *useremail.UserEmailer EnableRegistration bool + EnableClientRegistration bool localConnectorID string } @@ -110,19 +111,15 @@ func (s *Server) KillSession(sessionKey string) error { return err } -func (s *Server) pathURL(path string) *url.URL { - u := s.IssuerURL - u.Path = path - return &u -} - func (s *Server) ProviderConfig() oidc.ProviderConfig { + authEndpoint := s.absURL(httpPathAuth) + tokenEndpoint := s.absURL(httpPathToken) + keysEndpoint := s.absURL(httpPathKeys) cfg := oidc.ProviderConfig{ - Issuer: &s.IssuerURL, - - AuthEndpoint: s.pathURL(httpPathAuth), - TokenEndpoint: s.pathURL(httpPathToken), - KeysEndpoint: s.pathURL(httpPathKeys), + Issuer: &s.IssuerURL, + AuthEndpoint: &authEndpoint, + TokenEndpoint: &tokenEndpoint, + KeysEndpoint: &keysEndpoint, GrantTypesSupported: []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeClientCreds}, ResponseTypesSupported: []string{"code"}, @@ -131,6 +128,11 @@ func (s *Server) ProviderConfig() oidc.ProviderConfig { TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, } + if s.EnableClientRegistration { + regEndpoint := s.absURL(httpPathClientRegistration) + cfg.RegistrationEndpoint = ®Endpoint + } + return cfg } @@ -246,6 +248,10 @@ func (s *Server) HTTPHandler() http.Handler { redirectValidityWindow: s.SessionManager.ValidityWindow, }) + if s.EnableClientRegistration { + mux.HandleFunc(httpPathClientRegistration, s.handleClientRegistration) + } + mux.HandleFunc(httpPathDebugVars, health.ExpvarHandler) pcfg := s.ProviderConfig()