diff --git a/cmd/poke/config.go b/cmd/poke/config.go index fc06b208..89d1ae21 100644 --- a/cmd/poke/config.go +++ b/cmd/poke/config.go @@ -7,6 +7,7 @@ import ( "github.com/coreos/poke/connector/github" "github.com/coreos/poke/connector/ldap" "github.com/coreos/poke/connector/mock" + "github.com/coreos/poke/connector/oidc" "github.com/coreos/poke/storage" "github.com/coreos/poke/storage/kubernetes" "github.com/coreos/poke/storage/memory" @@ -100,33 +101,34 @@ func (c *Connector) UnmarshalYAML(unmarshal func(interface{}) error) error { c.Name = connectorMetadata.Name c.ID = connectorMetadata.ID + var err error switch c.Type { case "mock": var config struct { Config mock.Config `yaml:"config"` } - if err := unmarshal(&config); err != nil { - return err - } + err = unmarshal(&config) c.Config = &config.Config case "ldap": var config struct { Config ldap.Config `yaml:"config"` } - if err := unmarshal(&config); err != nil { - return err - } + err = unmarshal(&config) c.Config = &config.Config case "github": var config struct { Config github.Config `yaml:"config"` } - if err := unmarshal(&config); err != nil { - return err + err = unmarshal(&config) + c.Config = &config.Config + case "oidc": + var config struct { + Config oidc.Config `yaml:"config"` } + err = unmarshal(&config) c.Config = &config.Config default: return fmt.Errorf("unknown connector type %q", c.Type) } - return nil + return err } diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go index 41bef5eb..17aef1e2 100644 --- a/connector/oidc/oidc.go +++ b/connector/oidc/oidc.go @@ -1,2 +1,133 @@ // Package oidc implements logging in through OpenID Connect providers. package oidc + +import ( + "errors" + "fmt" + "net/http" + "os" + + "github.com/ericchiang/oidc" + "golang.org/x/net/context" + "golang.org/x/oauth2" + + "github.com/coreos/poke/connector" +) + +// Config holds configuration options for OpenID Connect logins. +type Config struct { + Issuer string `yaml:"issuer"` + ClientID string `yaml:"clientID"` + ClientSecret string `yaml:"clientSecret"` + RedirectURI string `yaml:"redirectURI"` + + Scopes []string `yaml:"scopes"` // defaults to "profile" and "email" +} + +// Open returns a connector which can be used to login users through an upstream +// OpenID Connect provider. +func (c *Config) Open() (conn connector.Connector, err error) { + ctx, cancel := context.WithCancel(context.Background()) + + provider, err := oidc.NewProvider(ctx, c.Issuer) + if err != nil { + cancel() + return nil, fmt.Errorf("failed to get provider: %v", err) + } + + scopes := []string{oidc.ScopeOpenID} + if len(c.Scopes) > 0 { + scopes = append(scopes, c.Scopes...) + } else { + scopes = append(scopes, "profile", "email") + } + + clientID := os.ExpandEnv(c.ClientID) + return &oidcConnector{ + redirectURI: c.RedirectURI, + oauth2Config: &oauth2.Config{ + ClientID: clientID, + ClientSecret: os.ExpandEnv(c.ClientSecret), + Endpoint: provider.Endpoint(), + Scopes: scopes, + RedirectURL: c.RedirectURI, + }, + verifier: provider.NewVerifier(ctx, + oidc.VerifyExpiry(), + oidc.VerifyAudience(clientID), + ), + }, nil +} + +var ( + _ connector.CallbackConnector = (*oidcConnector)(nil) +) + +type oidcConnector struct { + redirectURI string + oauth2Config *oauth2.Config + verifier *oidc.IDTokenVerifier + ctx context.Context + cancel context.CancelFunc +} + +func (c *oidcConnector) Close() error { + c.cancel() + return nil +} + +func (c *oidcConnector) LoginURL(callbackURL, state string) (string, error) { + if c.redirectURI != callbackURL { + return "", fmt.Errorf("expected callback URL did not match the URL in the config") + } + return c.oauth2Config.AuthCodeURL(state), nil +} + +type oauth2Error struct { + error string + errorDescription string +} + +func (e *oauth2Error) Error() string { + if e.errorDescription == "" { + return e.error + } + return e.error + ": " + e.errorDescription +} + +func (c *oidcConnector) HandleCallback(r *http.Request) (identity connector.Identity, state string, err error) { + q := r.URL.Query() + if errType := q.Get("error"); errType != "" { + return identity, "", &oauth2Error{errType, q.Get("error_description")} + } + token, err := c.oauth2Config.Exchange(c.ctx, q.Get("code")) + if err != nil { + return identity, "", fmt.Errorf("oidc: failed to get token: %v", err) + } + + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + return identity, "", errors.New("oidc: no id_token in token response") + } + idToken, err := c.verifier.Verify(rawIDToken) + if err != nil { + return identity, "", fmt.Errorf("oidc: failed to verify ID Token: %v", err) + } + + var claims struct { + Username string `json:"name"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + } + if err := idToken.Claims(&claims); err != nil { + return identity, "", fmt.Errorf("oidc: failed to decode claims: %v", err) + } + + identity = connector.Identity{ + UserID: idToken.Subject, + Username: claims.Username, + Email: claims.Email, + EmailVerified: claims.EmailVerified, + } + return identity, q.Get("state"), nil +} diff --git a/example/config-dev.yaml b/example/config-dev.yaml index d3d01afb..65267d31 100644 --- a/example/config-dev.yaml +++ b/example/config-dev.yaml @@ -18,6 +18,14 @@ connectors: clientSecret: "$GITHUB_CLIENT_SECRET" redirectURI: http://127.0.0.1:5556/callback/github org: kubernetes +- type: oidc + id: google + name: Google Account + config: + issuer: https://accounts.google.com + clientID: "$GOOGLE_OAUTH2_CLIENT_ID" + clientSecret: "$GOOGLE_OAUTH2_CLIENT_SECRET" + redirectURI: http://127.0.0.1:5556/callback/google staticClients: - id: example-app diff --git a/glide.lock b/glide.lock index a13f9ffd..6fbfbddc 100644 --- a/glide.lock +++ b/glide.lock @@ -1,8 +1,8 @@ -hash: 4442a097b81856345ae5f80101ad1a692a0b4e5d9b7627f5ad09cd20926122f4 -updated: 2016-08-05T09:58:15.61704222-07:00 +hash: 2af4a276277d2ab2ba9de9b0fd67ab7d6b70c07f4171a9efb225f30306d6f3eb +updated: 2016-08-08T11:20:44.300140564-07:00 imports: - name: github.com/ericchiang/oidc - version: 69fec81d167d815f4f455c741b2a94ffaf547ed2 + version: 1907f0e61549f9081f26bdf269f11603496c9dee - name: github.com/golang/protobuf version: 874264fbbb43f4d91e999fecb4b40143ed611400 subpackages: diff --git a/glide.yaml b/glide.yaml index 14058f67..a10eeae6 100644 --- a/glide.yaml +++ b/glide.yaml @@ -43,7 +43,7 @@ import: - bcrypt - package: github.com/ericchiang/oidc - version: 69fec81d167d815f4f455c741b2a94ffaf547ed2 + version: 1907f0e61549f9081f26bdf269f11603496c9dee - package: github.com/pquerna/cachecontrol version: c97913dcbd76de40b051a9b4cd827f7eaeb7a868 - package: gopkg.in/square/go-jose.v1 diff --git a/vendor/github.com/ericchiang/oidc/.travis.yml b/vendor/github.com/ericchiang/oidc/.travis.yml new file mode 100644 index 00000000..9d73a45f --- /dev/null +++ b/vendor/github.com/ericchiang/oidc/.travis.yml @@ -0,0 +1,13 @@ +language: go + +go: +- 1.5.4 +- 1.6.3 +- tip + +notifications: + email: false + +matrix: + allow_failures: + - go: tip diff --git a/vendor/github.com/ericchiang/oidc/README.md b/vendor/github.com/ericchiang/oidc/README.md index 6c665209..c6adbbf3 100644 --- a/vendor/github.com/ericchiang/oidc/README.md +++ b/vendor/github.com/ericchiang/oidc/README.md @@ -58,6 +58,13 @@ Or the provider can be used to verify and inspect the OpenID Connect verifier := provider.NewVerifier(ctx) ``` +The verifier itself can be constructed with addition checks, such as verifing a +token was issued for a specific client or hasn't expired. + +```go +verifier := provier.NewVerifier(ctx, oidc.VerifyAudience(clientID), oidc.VerifyExpiry()) +``` + The returned verifier can be used to ensure the ID Token (a JWT) is signed by the provider. ```go @@ -78,19 +85,19 @@ func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) { } // Verify that the ID Token is signed by the provider. - payload, err := verifier.Verify(rawIDToken) + idToken, err := verifier.Verify(rawIDToken) if err != nil { http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) return } // Unmarshal ID Token for expected custom claims. - var idToken struct { + var claims struct { Email string `json:"email"` EmailVerified bool `json:"email_verified"` } - if err := json.Unmarshal(payload, &idToken); err != nil { - http.Error(w, "Failed to unmarshal ID Token: "+err.Error(), http.StatusInternalServerError) + if err := idToken.Claims(&claims); err != nil { + http.Error(w, "Failed to unmarshal ID Token claims: "+err.Error(), http.StatusInternalServerError) return } diff --git a/vendor/github.com/ericchiang/oidc/doc.go b/vendor/github.com/ericchiang/oidc/doc.go index e891726a..cbc1ec4f 100644 --- a/vendor/github.com/ericchiang/oidc/doc.go +++ b/vendor/github.com/ericchiang/oidc/doc.go @@ -65,19 +65,19 @@ including verifying the JWT signature. It then returns the payload. } // Verify that the ID Token is signed by the provider. - payload, err := verifier.Verify(rawIDToken) + idToken, err := verifier.Verify(rawIDToken) if err != nil { http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) return } // Unmarshal ID Token for expected custom claims. - var idToken struct { + var claims struct { Email string `json:"email"` EmailVerified bool `json:"email_verified"` } - if err := json.Unmarshal(payload, &idToken); err != nil { - http.Error(w, "Failed to unmarshal ID Token: "+err.Error(), http.StatusInternalServerError) + if err := idToken.Claims(&claims); err != nil { + http.Error(w, "Failed to unmarshal ID Token custom claims: "+err.Error(), http.StatusInternalServerError) return } @@ -123,7 +123,7 @@ The nonce enabled verifier can then be used to verify the nonce while unpacking } // Verify that the ID Token is signed by the provider and verify the nonce. - payload, err := nonceEnabledVerifier.Verify(rawIDToken) + idToken, err := nonceEnabledVerifier.Verify(rawIDToken) if err != nil { http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) return diff --git a/vendor/github.com/ericchiang/oidc/examples/idtoken/app.go b/vendor/github.com/ericchiang/oidc/examples/idtoken/app.go index ed0750cb..b2a42a76 100644 --- a/vendor/github.com/ericchiang/oidc/examples/idtoken/app.go +++ b/vendor/github.com/ericchiang/oidc/examples/idtoken/app.go @@ -59,8 +59,7 @@ func main() { http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError) return } - log.Println(rawIDToken) - idTokenPayload, err := verifier.Verify(rawIDToken) + idToken, err := verifier.Verify(rawIDToken) if err != nil { http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) return @@ -68,11 +67,15 @@ func main() { oauth2Token.AccessToken = "*REDACTED*" - rawMessage := json.RawMessage(idTokenPayload) resp := struct { OAuth2Token *oauth2.Token IDTokenClaims *json.RawMessage // ID Token payload is just JSON. - }{oauth2Token, &rawMessage} + }{oauth2Token, new(json.RawMessage)} + + if err := idToken.Claims(&resp.IDTokenClaims); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } data, err := json.MarshalIndent(resp, "", " ") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/vendor/github.com/ericchiang/oidc/examples/nonce/app.go b/vendor/github.com/ericchiang/oidc/examples/nonce/app.go index 53e15adc..2167f962 100644 --- a/vendor/github.com/ericchiang/oidc/examples/nonce/app.go +++ b/vendor/github.com/ericchiang/oidc/examples/nonce/app.go @@ -69,23 +69,28 @@ func main() { http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError) return } + rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError) return } // Verify the ID Token signature and nonce. - idTokenPayload, err := nonceEnabledVerifier.Verify(rawIDToken) + idToken, err := nonceEnabledVerifier.Verify(rawIDToken) if err != nil { http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) return } - rawMessage := json.RawMessage(idTokenPayload) resp := struct { - OAuth2Token *oauth2.Token - IDToken *json.RawMessage // ID Token payload is just JSON. - }{oauth2Token, &rawMessage} + OAuth2Token *oauth2.Token + IDTokenClaims *json.RawMessage // ID Token payload is just JSON. + }{oauth2Token, new(json.RawMessage)} + + if err := idToken.Claims(&resp.IDTokenClaims); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } data, err := json.MarshalIndent(resp, "", " ") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/vendor/github.com/ericchiang/oidc/nonce.go b/vendor/github.com/ericchiang/oidc/nonce.go index 3426065e..c0bb213f 100644 --- a/vendor/github.com/ericchiang/oidc/nonce.go +++ b/vendor/github.com/ericchiang/oidc/nonce.go @@ -1,9 +1,7 @@ package oidc import ( - "encoding/json" "errors" - "fmt" "golang.org/x/oauth2" ) @@ -29,13 +27,7 @@ type nonceVerifier struct { nonceSource NonceSource } -func (n nonceVerifier) verifyIDTokenPayload(payload []byte) error { - var token struct { - Nonce string `json:"nonce"` - } - if err := json.Unmarshal(payload, &token); err != nil { - return fmt.Errorf("oidc: failed to unmarshal nonce: %v", err) - } +func (n nonceVerifier) verifyIDToken(token *IDToken) error { if token.Nonce == "" { return errors.New("oidc: no nonce present in ID Token") } diff --git a/vendor/github.com/ericchiang/oidc/oidc.go b/vendor/github.com/ericchiang/oidc/oidc.go index 4ebf96e2..c2c7eea6 100644 --- a/vendor/github.com/ericchiang/oidc/oidc.go +++ b/vendor/github.com/ericchiang/oidc/oidc.go @@ -15,9 +15,9 @@ import ( var ( // ErrTokenExpired indicates that a token parsed by a verifier has expired. - ErrTokenExpired = errors.New("ID Token expired") + ErrTokenExpired = errors.New("oidc: ID Token expired") // ErrNotSupported indicates that the requested optional OpenID Connect endpoint is not supported by the provider. - ErrNotSupported = errors.New("endpoint not supported") + ErrNotSupported = errors.New("oidc: endpoint not supported") ) const ( @@ -44,8 +44,8 @@ type Provider struct { JWKSURL string `json:"jwks_uri"` UserInfoURL string `json:"userinfo_endpoint"` - // Optionally contains extra claims. - raw map[string]interface{} + // Raw claims returned by the server. + rawClaims []byte } // NewProvider uses the OpenID Connect disovery mechanism to construct a Provider. @@ -67,20 +67,19 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) { if err := json.Unmarshal(body, &p); err != nil { return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err) } - // raw claims do not get error checks - json.Unmarshal(body, &p.raw) + p.rawClaims = body if p.Issuer != issuer { return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer) } return &p, nil } -// Extra returns additional fields returned by the server during discovery. -func (p *Provider) Extra(key string) interface{} { - if p.raw != nil { - return p.raw[key] +// Claims returns additional fields returned by the server during discovery. +func (p *Provider) Claims(v interface{}) error { + if p.rawClaims == nil { + return errors.New("oidc: claims not set") } - return nil + return json.Unmarshal(p.rawClaims, v) } // Endpoint returns the OAuth2 auth and token endpoints for the given provider. @@ -95,16 +94,15 @@ type UserInfo struct { Email string `json:"email"` EmailVerified bool `json:"email_verified"` - // Optionally contains extra claims. - raw map[string]interface{} + claims []byte } -// Extra returns additional claims returned by the server. -func (u *UserInfo) Extra(key string) interface{} { - if u.raw != nil { - return u.raw[key] +// Claims unmarshals the raw JSON object claims into the provided object. +func (u *UserInfo) Claims(v interface{}) error { + if u.claims == nil { + return errors.New("oidc: claims not set") } - return nil + return json.Unmarshal(u.claims, v) } // UserInfo uses the token source to query the provider's user info endpoint. @@ -130,11 +128,101 @@ func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) if err := json.Unmarshal(body, &userInfo); err != nil { return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err) } - // raw claims do not get error checks - json.Unmarshal(body, &userInfo.raw) + userInfo.claims = body return &userInfo, nil } +// IDToken is an OpenID Connect extension that provides a predictable representation +// of an authorization event. +// +// The ID Token only holds fields OpenID Connect requires. To access additional +// claims returned by the server, use the Claims method. +// +// idToken, err := idTokenVerifier.Verify(rawIDToken) +// if err != nil { +// // handle error +// } +// var claims struct { +// Email string `json:"email"` +// EmailVerified bool `json:"email_verified"` +// } +// if err := idToken.Claims(&claims); err != nil { +// // handle error +// } +// +type IDToken struct { + // The URL of the server which issued this token. This will always be the same + // as the URL used for initial discovery. + Issuer string + + // The client, or set of clients, that this token is issued for. + Audience []string + + // A unique string which identifies the end user. + Subject string + + IssuedAt time.Time + Expiry time.Time + Nonce string + + claims []byte +} + +// Claims unmarshals the raw JSON payload of the ID Token into a provided struct. +func (i *IDToken) Claims(v interface{}) error { + if i.claims == nil { + return errors.New("oidc: claims not set") + } + return json.Unmarshal(i.claims, v) +} + +type audience []string + +func (a *audience) UnmarshalJSON(b []byte) error { + var s string + if json.Unmarshal(b, &s) == nil { + *a = audience{s} + return nil + } + var auds []string + if err := json.Unmarshal(b, &auds); err != nil { + return err + } + *a = audience(auds) + return nil +} + +type jsonTime time.Time + +func (j *jsonTime) UnmarshalJSON(b []byte) error { + var n json.Number + if err := json.Unmarshal(b, &n); err != nil { + return err + } + var unix int64 + + if t, err := n.Int64(); err == nil { + unix = t + } else { + f, err := n.Float64() + if err != nil { + return err + } + unix = int64(f) + } + *j = jsonTime(time.Unix(unix, 0)) + return nil +} + +type idToken struct { + Issuer string `json:"iss"` + Subject string `json:"sub"` + Audience audience `json:"aud"` + Expiry jsonTime `json:"exp"` + IssuedAt jsonTime `json:"iat"` + Nonce string `json:"nonce"` +} + // IDTokenVerifier provides verification for ID Tokens. type IDTokenVerifier struct { issuer string @@ -143,31 +231,34 @@ type IDTokenVerifier struct { } // Verify parse the raw ID Token, verifies it's been signed by the provider, preforms -// additional verification, such as checking the expiration, and returns the claims. -func (v *IDTokenVerifier) Verify(rawIDToken string) (payload []byte, err error) { - payload, err = v.keySet.verifyJWT(rawIDToken) +// additional verification, and returns the claims. +func (v *IDTokenVerifier) Verify(rawIDToken string) (*IDToken, error) { + payload, err := v.keySet.verifyJWT(rawIDToken) if err != nil { return nil, err } - var token struct { - Exp float64 `json:"exp"` // JSON numbers are always float64s. - Issuer string `json:"iss"` - } + var token idToken if err := json.Unmarshal(payload, &token); err != nil { return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err) } if v.issuer != token.Issuer { return nil, fmt.Errorf("oidc: iss field did not match provider issuer") } - if time.Unix(int64(token.Exp), 0).Before(time.Now().Round(time.Second)) { - return nil, ErrTokenExpired + t := &IDToken{ + Issuer: token.Issuer, + Subject: token.Subject, + Audience: []string(token.Audience), + Expiry: time.Time(token.Expiry), + IssuedAt: time.Time(token.Expiry), + Nonce: token.Nonce, + claims: payload, } for _, option := range v.options { - if err := option.verifyIDTokenPayload(payload); err != nil { + if err := option.verifyIDToken(t); err != nil { return nil, err } } - return payload, nil + return t, nil } // NewVerifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs. @@ -184,7 +275,7 @@ func (p *Provider) NewVerifier(ctx context.Context, options ...VerificationOptio // VerificationOption is an option provided to Provider.NewVerifier. type VerificationOption interface { - verifyIDTokenPayload(raw []byte) error + verifyIDToken(token *IDToken) error } // VerifyAudience ensures that an ID Token was issued for the specific client. @@ -199,25 +290,8 @@ type clientVerifier struct { clientID string } -func (c clientVerifier) verifyIDTokenPayload(payload []byte) error { - var token struct { - Aud string `json:"aud"` - } - if err := json.Unmarshal(payload, &token); err == nil { - if token.Aud != c.clientID { - return errors.New("oidc: id token aud field did not match client_id") - } - return nil - } - - // Aud can optionally be an array of strings - var token2 struct { - Aud []string `json:"aud"` - } - if err := json.Unmarshal(payload, &token2); err != nil { - return fmt.Errorf("oidc: failed to unmarshal aud claim: %v", err) - } - for _, aud := range token2.Aud { +func (c clientVerifier) verifyIDToken(token *IDToken) error { + for _, aud := range token.Audience { if aud == c.clientID { return nil } @@ -225,6 +299,22 @@ func (c clientVerifier) verifyIDTokenPayload(payload []byte) error { return errors.New("oidc: id token aud field did not match client_id") } +// VerifyExpiry ensures that an ID Token has not expired. +func VerifyExpiry() VerificationOption { + return expiryVerifier{time.Now} +} + +type expiryVerifier struct { + now func() time.Time +} + +func (e expiryVerifier) verifyIDToken(token *IDToken) error { + if e.now().After(token.Expiry) { + return ErrTokenExpired + } + return nil +} + // This method is internal to golang.org/x/oauth2. Just copy it. func contextClient(ctx context.Context) *http.Client { if ctx != nil { diff --git a/vendor/github.com/ericchiang/oidc/oidc_test.go b/vendor/github.com/ericchiang/oidc/oidc_test.go index 97a8b048..3a31646f 100644 --- a/vendor/github.com/ericchiang/oidc/oidc_test.go +++ b/vendor/github.com/ericchiang/oidc/oidc_test.go @@ -1,49 +1,40 @@ package oidc -import "testing" +import ( + "encoding/json" + "reflect" + "testing" +) func TestClientVerifier(t *testing.T) { tests := []struct { clientID string - payload string + aud []string wantErr bool }{ { clientID: "1", - payload: `{"aud":"1"}`, + aud: []string{"1"}, }, { clientID: "1", - payload: `{"aud":"2"}`, + aud: []string{"2"}, wantErr: true, }, { clientID: "1", - payload: `{"aud":["1"]}`, - }, - { - clientID: "1", - payload: `{"aud":["1", "2"]}`, + aud: []string{"2", "1"}, }, { clientID: "3", - payload: `{"aud":["1", "2"]}`, - wantErr: true, - }, - { - clientID: "3", - payload: `{"aud":}`, // invalid JSON - wantErr: true, - }, - { - clientID: "1", - payload: `{}`, + aud: []string{"1", "2"}, wantErr: true, }, } for i, tc := range tests { - err := (clientVerifier{tc.clientID}).verifyIDTokenPayload([]byte(tc.payload)) + token := IDToken{Audience: tc.aud} + err := (clientVerifier{tc.clientID}).verifyIDToken(&token) if err != nil && !tc.wantErr { t.Errorf("case %d: %v", i) } @@ -52,3 +43,34 @@ func TestClientVerifier(t *testing.T) { } } } + +func TestUnmarshalAudience(t *testing.T) { + tests := []struct { + data string + want audience + wantErr bool + }{ + {`"foo"`, audience{"foo"}, false}, + {`["foo","bar"]`, audience{"foo", "bar"}, false}, + {"foo", nil, true}, // invalid JSON + } + + for _, tc := range tests { + var a audience + if err := json.Unmarshal([]byte(tc.data), &a); err != nil { + if !tc.wantErr { + t.Errorf("failed to unmarshal %q: %v", tc.data, err) + } + continue + } + + if tc.wantErr { + t.Errorf("did not expected to be able to unmarshal %q", tc.data) + continue + } + + if !reflect.DeepEqual(tc.want, a) { + t.Errorf("from %q expected %q got %q", tc.data, tc.want, a) + } + } +} diff --git a/vendor/github.com/ericchiang/oidc/oidcproxy/main.go b/vendor/github.com/ericchiang/oidc/oidcproxy/main.go index 4691c947..fc54f8ed 100644 --- a/vendor/github.com/ericchiang/oidc/oidcproxy/main.go +++ b/vendor/github.com/ericchiang/oidc/oidcproxy/main.go @@ -3,7 +3,6 @@ package main import ( "crypto/rand" "encoding/gob" - "encoding/json" "flag" "fmt" "io" @@ -192,7 +191,7 @@ func handleCallback(w http.ResponseWriter, r *http.Request) { return httpError(http.StatusInternalServerError, "Authentication failed") } - payload, err := verifier.Verify(rawIDToken) + idToken, err := verifier.Verify(rawIDToken) if err != nil { log.Printf("Failed to verify token: %v", err) return httpError(http.StatusInternalServerError, "Authentication failed") @@ -201,7 +200,8 @@ func handleCallback(w http.ResponseWriter, r *http.Request) { Email string `json:"email"` EmailVerified bool `json:"email_verified"` } - if err := json.Unmarshal(payload, &claims); err != nil { + + if err := idToken.Claims(&claims); err != nil { log.Printf("Failed to decode claims: %v", err) return httpError(http.StatusInternalServerError, "Authentication failed") }