// 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/dex/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 }