diff --git a/cmd/poke/config.go b/cmd/poke/config.go
index fc06b208eeb292e8ef5ca4955b45a2b2b8da6e55..89d1ae218b4c8ac2e7f9c122575bc44f9345dbd4 100644
--- a/cmd/poke/config.go
+++ b/cmd/poke/config.go
@@ -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
 }
diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go
index 41bef5eb1ab11fa063d2543ec03d0f0dd9f1eed9..17aef1e2bddc55672c6123d8a706d31bbf341f00 100644
--- a/connector/oidc/oidc.go
+++ b/connector/oidc/oidc.go
@@ -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
+}
diff --git a/example/config-dev.yaml b/example/config-dev.yaml
index d3d01afb8a21452a43b2ca1639b4d9733520b3cc..65267d31e814ef1b115e7746a38d43e18ad41624 100644
--- a/example/config-dev.yaml
+++ b/example/config-dev.yaml
@@ -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