mirror of https://github.com/dexidp/dex.git
2 changed files with 192 additions and 0 deletions
@ -0,0 +1,190 @@
|
||||
// Package google implements logging in through Google's OpenID Connect provider.
|
||||
package google |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"net/http" |
||||
"time" |
||||
|
||||
"github.com/coreos/go-oidc" |
||||
"github.com/sirupsen/logrus" |
||||
"golang.org/x/oauth2" |
||||
|
||||
"github.com/dexidp/dex/connector" |
||||
) |
||||
|
||||
// Config holds configuration options for OpenID Connect logins.
|
||||
type Config struct { |
||||
Issuer string `json:"issuer"` |
||||
ClientID string `json:"clientID"` |
||||
ClientSecret string `json:"clientSecret"` |
||||
RedirectURI string `json:"redirectURI"` |
||||
|
||||
Scopes []string `json:"scopes"` // defaults to "profile" and "email"
|
||||
|
||||
// Optional list of whitelisted domains
|
||||
// If this field is nonempty, only users from a listed domain will be allowed to log in
|
||||
HostedDomains []string `json:"hostedDomains"` |
||||
} |
||||
|
||||
// Open returns a connector which can be used to login users through an upstream
|
||||
// OpenID Connect provider.
|
||||
func (c *Config) Open(id string, logger logrus.FieldLogger) (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 &googleConnector{ |
||||
redirectURI: c.RedirectURI, |
||||
oauth2Config: &oauth2.Config{ |
||||
ClientID: clientID, |
||||
ClientSecret: c.ClientSecret, |
||||
Endpoint: provider.Endpoint(), |
||||
Scopes: scopes, |
||||
RedirectURL: c.RedirectURI, |
||||
}, |
||||
verifier: provider.Verifier( |
||||
&oidc.Config{ClientID: clientID}, |
||||
), |
||||
logger: logger, |
||||
cancel: cancel, |
||||
hostedDomains: c.HostedDomains, |
||||
}, nil |
||||
} |
||||
|
||||
var ( |
||||
_ connector.CallbackConnector = (*googleConnector)(nil) |
||||
_ connector.RefreshConnector = (*googleConnector)(nil) |
||||
) |
||||
|
||||
type googleConnector struct { |
||||
redirectURI string |
||||
oauth2Config *oauth2.Config |
||||
verifier *oidc.IDTokenVerifier |
||||
ctx context.Context |
||||
cancel context.CancelFunc |
||||
logger logrus.FieldLogger |
||||
hostedDomains []string |
||||
} |
||||
|
||||
func (c *googleConnector) Close() error { |
||||
c.cancel() |
||||
return nil |
||||
} |
||||
|
||||
func (c *googleConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) { |
||||
if c.redirectURI != callbackURL { |
||||
return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) |
||||
} |
||||
|
||||
var opts []oauth2.AuthCodeOption |
||||
if len(c.hostedDomains) > 0 { |
||||
preferredDomain := c.hostedDomains[0] |
||||
if len(c.hostedDomains) > 1 { |
||||
preferredDomain = "*" |
||||
} |
||||
opts = append(opts, oauth2.SetAuthURLParam("hd", preferredDomain)) |
||||
} |
||||
|
||||
if s.OfflineAccess { |
||||
opts = append(opts, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent")) |
||||
} |
||||
return c.oauth2Config.AuthCodeURL(state, opts...), 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 *googleConnector) HandleCallback(s connector.Scopes, 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(r.Context(), q.Get("code")) |
||||
if err != nil { |
||||
return identity, fmt.Errorf("google: failed to get token: %v", err) |
||||
} |
||||
|
||||
return c.createIdentity(r.Context(), identity, token) |
||||
} |
||||
|
||||
// Refresh is implemented for backwards compatibility, even though it's a no-op.
|
||||
func (c *googleConnector) Refresh(ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) { |
||||
t := &oauth2.Token{ |
||||
RefreshToken: string(identity.ConnectorData), |
||||
Expiry: time.Now().Add(-time.Hour), |
||||
} |
||||
token, err := c.oauth2Config.TokenSource(ctx, t).Token() |
||||
if err != nil { |
||||
return identity, fmt.Errorf("google: failed to get token: %v", err) |
||||
} |
||||
|
||||
return c.createIdentity(ctx, identity, token) |
||||
} |
||||
|
||||
func (c *googleConnector) createIdentity(ctx context.Context, identity connector.Identity, token *oauth2.Token) (connector.Identity, error) { |
||||
rawIDToken, ok := token.Extra("id_token").(string) |
||||
if !ok { |
||||
return identity, errors.New("google: no id_token in token response") |
||||
} |
||||
idToken, err := c.verifier.Verify(ctx, rawIDToken) |
||||
if err != nil { |
||||
return identity, fmt.Errorf("google: failed to verify ID Token: %v", err) |
||||
} |
||||
|
||||
var claims struct { |
||||
Username string `json:"name"` |
||||
Email string `json:"email"` |
||||
EmailVerified bool `json:"email_verified"` |
||||
HostedDomain string `json:"hd"` |
||||
} |
||||
if err := idToken.Claims(&claims); err != nil { |
||||
return identity, fmt.Errorf("oidc: failed to decode claims: %v", err) |
||||
} |
||||
|
||||
if len(c.hostedDomains) > 0 { |
||||
found := false |
||||
for _, domain := range c.hostedDomains { |
||||
if claims.HostedDomain == domain { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
|
||||
if !found { |
||||
return identity, fmt.Errorf("oidc: unexpected hd claim %v", claims.HostedDomain) |
||||
} |
||||
} |
||||
|
||||
identity = connector.Identity{ |
||||
UserID: idToken.Subject, |
||||
Username: claims.Username, |
||||
Email: claims.Email, |
||||
EmailVerified: claims.EmailVerified, |
||||
ConnectorData: []byte(token.RefreshToken), |
||||
} |
||||
return identity, nil |
||||
} |
||||
Loading…
Reference in new issue