*: implement the OpenID Connect connector
This commit is contained in:
		| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user