*: 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/github" | ||||||
| 	"github.com/coreos/poke/connector/ldap" | 	"github.com/coreos/poke/connector/ldap" | ||||||
| 	"github.com/coreos/poke/connector/mock" | 	"github.com/coreos/poke/connector/mock" | ||||||
|  | 	"github.com/coreos/poke/connector/oidc" | ||||||
| 	"github.com/coreos/poke/storage" | 	"github.com/coreos/poke/storage" | ||||||
| 	"github.com/coreos/poke/storage/kubernetes" | 	"github.com/coreos/poke/storage/kubernetes" | ||||||
| 	"github.com/coreos/poke/storage/memory" | 	"github.com/coreos/poke/storage/memory" | ||||||
| @@ -100,33 +101,34 @@ func (c *Connector) UnmarshalYAML(unmarshal func(interface{}) error) error { | |||||||
| 	c.Name = connectorMetadata.Name | 	c.Name = connectorMetadata.Name | ||||||
| 	c.ID = connectorMetadata.ID | 	c.ID = connectorMetadata.ID | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
| 	switch c.Type { | 	switch c.Type { | ||||||
| 	case "mock": | 	case "mock": | ||||||
| 		var config struct { | 		var config struct { | ||||||
| 			Config mock.Config `yaml:"config"` | 			Config mock.Config `yaml:"config"` | ||||||
| 		} | 		} | ||||||
| 		if err := unmarshal(&config); err != nil { | 		err = unmarshal(&config) | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		c.Config = &config.Config | 		c.Config = &config.Config | ||||||
| 	case "ldap": | 	case "ldap": | ||||||
| 		var config struct { | 		var config struct { | ||||||
| 			Config ldap.Config `yaml:"config"` | 			Config ldap.Config `yaml:"config"` | ||||||
| 		} | 		} | ||||||
| 		if err := unmarshal(&config); err != nil { | 		err = unmarshal(&config) | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		c.Config = &config.Config | 		c.Config = &config.Config | ||||||
| 	case "github": | 	case "github": | ||||||
| 		var config struct { | 		var config struct { | ||||||
| 			Config github.Config `yaml:"config"` | 			Config github.Config `yaml:"config"` | ||||||
| 		} | 		} | ||||||
| 		if err := unmarshal(&config); err != nil { | 		err = unmarshal(&config) | ||||||
| 			return err | 		c.Config = &config.Config | ||||||
|  | 	case "oidc": | ||||||
|  | 		var config struct { | ||||||
|  | 			Config oidc.Config `yaml:"config"` | ||||||
| 		} | 		} | ||||||
|  | 		err = unmarshal(&config) | ||||||
| 		c.Config = &config.Config | 		c.Config = &config.Config | ||||||
| 	default: | 	default: | ||||||
| 		return fmt.Errorf("unknown connector type %q", c.Type) | 		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 implements logging in through OpenID Connect providers. | ||||||
| package oidc | 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" |     clientSecret: "$GITHUB_CLIENT_SECRET" | ||||||
|     redirectURI: http://127.0.0.1:5556/callback/github |     redirectURI: http://127.0.0.1:5556/callback/github | ||||||
|     org: kubernetes |     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: | staticClients: | ||||||
| - id: example-app | - id: example-app | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user