Skip to content
Snippets Groups Projects
oidc.go 3.31 KiB
Newer Older
  • Learn to ignore specific revisions
  • Eric Chiang's avatar
    Eric Chiang committed
    // Package oidc implements logging in through OpenID Connect providers.
    package oidc
    
    
    import (
    	"errors"
    	"fmt"
    	"net/http"
    
    	"github.com/ericchiang/oidc"
    	"golang.org/x/net/context"
    	"golang.org/x/oauth2"
    
    
    Eric Chiang's avatar
    Eric Chiang committed
    	"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 := c.ClientID
    
    	return &oidcConnector{
    		redirectURI: c.RedirectURI,
    		oauth2Config: &oauth2.Config{
    			ClientID:     clientID,
    
    			ClientSecret: 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, 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,
    	}