Repository: coreos/dex
Pull request #1072 (2017-09-19 22:32:37)

*: run kubernetes tests in travis
Just an experiment for now

rithujohn191: Do you want to add a note here https://github.com/coreos/dex/blob/master/Documentation/dev-integration-tests.md mentioning that we have added these tests to travis?
ericchiang: @rithujohn191 ready for review

Patch:
diff --git a/.dockerignore b/.dockerignore
index ba077a40..1c99e92d 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,1 +1,3 @@
-bin
+*
+!_output/bin
+!web
diff --git a/.travis.yml b/.travis.yml
index b753040a..f5554e59 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,11 +3,12 @@ language: go
 sudo: required
 
 go:
-  - 1.8
-  - 1.9
+  - 1.7.5
+  - 1.8.3
 
 services:
   - postgresql
+  - docker
 
 env:
   - DEX_POSTGRES_DATABASE=postgres DEX_POSTGRES_USER=postgres DEX_POSTGRES_HOST="localhost" DEX_LDAP_TESTS=1 DEBIAN_FRONTEND=noninteractive
@@ -21,6 +22,7 @@ install:
 
 script:
   - make testall
+  - ./scripts/test-k8s.sh
 
 notifications:
   email: false
diff --git a/Dockerfile b/Dockerfile
index eec1acbe..7ab96f92 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,9 @@
-FROM golang:1.8.3-alpine
+FROM alpine:3.4
 
 MAINTAINER Ed Rooth <ed.rooth@coreos.com>
 MAINTAINER Lucas Servén <lucas.serven@coreos.com>
 MAINTAINER Rithu John <rithu.john@coreos.com>
 
-RUN apk add --no-cache --update alpine-sdk
-
-COPY . /go/src/github.com/coreos/dex
-RUN cd /go/src/github.com/coreos/dex && make release-binary
-
-FROM alpine:3.4
 # Dex connectors, such as GitHub and Google logins require root certificates.
 # Proper installations should manage those certificates, but it's a bad user
 # experience when this doesn't work out of the box.
@@ -17,7 +11,7 @@ FROM alpine:3.4
 # OpenSSL is required so wget can query HTTPS endpoints for health checking.
 RUN apk add --update ca-certificates openssl
 
-COPY --from=0 /go/bin/dex /usr/local/bin/dex
+COPY _output/bin/dex /usr/local/bin/dex
 
 # Import frontend assets and set the correct CWD directory so the assets
 # are in the default path.
diff --git a/Documentation/authproxy.md b/Documentation/authproxy.md
deleted file mode 100644
index 2e12ab09..00000000
--- a/Documentation/authproxy.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# Authenticating proxy
-
-NOTE: This connector is experimental and may change in the future.
-
-## Overview
-
-The `authproxy` connector returns identities based on authentication which your
-front-end web server performs. Dex consumes the `X-Remote-User` header set by
-the proxy, which is then used as the user's email address.
-
-__The proxy MUST remove any `X-Remote-*` headers set by the client, for any URL
-path, before the request is forwarded to dex.__
-
-The connector does not support refresh tokens or groups.
-
-## Configuration
-
-The `authproxy` connector is used by proxies to implement login strategies not
-supported by dex. For example, a proxy could handle a different OAuth2 strategy
-such as Slack. The connector takes no configuration other than a `name` and `id`:
-
-```yaml
-connectors:
-# Slack login implemented by an authenticating proxy, not by dex.
-- type: authproxy
-  id: slack
-  name: Slack 
-```
-
-The proxy only needs to authenticate the user when they attempt to visit the
-callback URL path:
-
-```
-( dex issuer URL )/callback/( connector id )?( url query )
-```
-
-For example, if dex is running at `https://auth.example.com/dex` and the connector
-ID is `slack`, the callback URL would look like:
-
-```
-https://auth.example.com/dex/callback/slack?state=xdg3z6quhrhwaueo5iysvliqf
-``` 
-
-The proxy should login the user then return them to the exact URL (inlucing the
-query), setting `X-Remote-User` to the user's email before proxying the request
-to dex.
-
-## Configuration example - Apache 2
-
-The following is an example config file that can be used by the external
-connector to authenticate a user.
-
-```yaml
-connectors:
-- type: authproxy
-  id: myBasicAuth
-  name: HTTP Basic Auth
-```
-
-The authproxy connector assumes that you configured your front-end web server
-such that it performs authentication for the `/dex/callback/myBasicAuth`
-location and provides the result in the X-Remote-User HTTP header. The following
-configuration will work for Apache 2.4.10+:
-
-```
-<Location /dex/>
-    ProxyPass "http://localhost:5556/dex/"
-    ProxyPassReverse "http://localhost:5556/dex/"
-
-    # Strip the X-Remote-User header from all requests except for the ones
-    # where we override it.
-    RequestHeader unset X-Remote-User
-</Location>
-
-<Location /dex/callback/myBasicAuth>
-    AuthType Basic
-    AuthName "db.debian.org webPassword"
-    AuthBasicProvider file
-    AuthUserFile "/etc/apache2/debian-web-pw.htpasswd"
-    Require valid-user
-
-    # Defense in depth: clear the Authorization header so that
-    # Debian Web Passwords never even reach dex.
-    RequestHeader unset Authorization
-
-    # Requires Apache 2.4.10+
-    RequestHeader set X-Remote-User expr=%{REMOTE_USER}@debian.org
-
-    ProxyPass "http://localhost:5556/dex/callback/myBasicAuth"
-    ProxyPassReverse "http://localhost:5556/dex/callback/myBasicAuth"
-</Location>
-```
-
-## Full Apache2 setup
-
-After installing your Linux distribution’s Apache2 package, place the following
-virtual host configuration in e.g. `/etc/apache2/sites-available/sso.conf`:
-
-```
-<VirtualHost sso.example.net>
-    ServerName sso.example.net
-
-    ServerAdmin webmaster@localhost
-    DocumentRoot /var/www/html
-
-    ErrorLog ${APACHE_LOG_DIR}/error.log
-    CustomLog ${APACHE_LOG_DIR}/access.log combined
-
-    <Location /dex/>
-        ProxyPass "http://localhost:5556/dex/"
-        ProxyPassReverse "http://localhost:5556/dex/"
-
-        # Strip the X-Remote-User header from all requests except for the ones
-        # where we override it.
-        RequestHeader unset X-Remote-User
-    </Location>
-
-    <Location /dex/callback/myBasicAuth>
-        AuthType Basic
-        AuthName "db.debian.org webPassword"
-        AuthBasicProvider file
-        AuthUserFile "/etc/apache2/debian-web-pw.htpasswd"
-        Require valid-user
-
-        # Defense in depth: clear the Authorization header so that
-        # Debian Web Passwords never even reach dex.
-        RequestHeader unset Authorization
-
-        # Requires Apache 2.4.10+
-        RequestHeader set X-Remote-User expr=%{REMOTE_USER}@debian.org
-
-        ProxyPass "http://localhost:5556/dex/callback/myBasicAuth"
-        ProxyPassReverse "http://localhost:5556/dex/callback/myBasicAuth"
-    </Location>
-</VirtualHost>
-```
-
-Then, enable it using `a2ensite sso.conf`, followed by a restart of Apache2.
diff --git a/Documentation/linkedin-connector.md b/Documentation/linkedin-connector.md
deleted file mode 100644
index 646ee243..00000000
--- a/Documentation/linkedin-connector.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Authentication through LinkedIn
-
-## Overview
-
-One of the login options for dex uses the LinkedIn OAuth2 flow to identify the end user through their LinkedIn account.
-
-When a client redeems a refresh token through dex, dex will re-query LinkedIn to update user information in the ID Token. To do this, __dex stores a readonly LinkedIn access token in its backing datastore.__ Users that reject dex's access through LinkedIn will also revoke all dex clients which authenticated them through LinkedIn.
-
-## Configuration
-
-Register a new application via `My Apps -> Create Application` ensuring the callback URL is `(dex issuer)/callback`. For example if dex is listening at the non-root path `https://auth.example.com/dex` the callback would be `https://auth.example.com/dex/callback`.
-
-The following is an example of a configuration for `examples/config-dev.yaml`:
-
-```yaml
-connectors:
-  - type: linkedin
-    # Required field for connector id.
-    id: linkedin
-    # Required field for connector name.
-    name: LinkedIn
-    config:
-      # Credentials can be string literals or pulled from the environment.
-      clientID: $LINKEDIN_APPLICATION_ID
-      clientSecret: $LINKEDIN_CLIENT_SECRET
-      redirectURI: http://127.0.0.1:5556/dex/callback
-```
diff --git a/Documentation/storage.md b/Documentation/storage.md
index 7c2abae8..3ebb72b9 100644
--- a/Documentation/storage.md
+++ b/Documentation/storage.md
@@ -53,36 +53,6 @@ status:
 
 Once the `CustomResourceDefinition` is created, custom resources can be created and stored at a namespace level. The CRD type and the custom resources can be queried, deleted, and edited like any other resource using `kubectl`.
 
-dex requires access to the non-namespaced `CustomResourceDefinition` type. For example, clusters using RBAC authorization would need to create the following roles and bindings:
-```
-apiVersion: rbac.authorization.k8s.io/v1beta1
-kind: ClusterRole
-metadata:
-  name: dex
-rules:
-- apiGroups: ["dex.coreos.com"] # API group created by dex
-  resources: ["*"]
-  verbs: ["*"]
-- apiGroups: ["apiextensions.k8s.io"]
-  resources: ["customresourcedefinitions"]
-  verbs: ["create"] # To manage its own resources identity must be able to create customresourcedefinitions.
----
-apiVersion: rbac.authorization.k8s.io/v1beta1
-kind: ClusterRoleBinding
-metadata:
-  name: dex
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: dex
-subjects:
-- kind: ServiceAccount
-  name: dex                 # Service account assigned to the dex pod.
-  namespace: dex-namespace  # The namespace dex is running in.
-
-```
-
-
 ## Kubernetes third party resources(TPRs)
 
 __NOTE:__ TPRs will be deprecated by Kubernetes version 1.8.
diff --git a/Makefile b/Makefile
index e762f456..9792c5b1 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,8 @@ DOCKER_REPO=quay.io/coreos/dex
 DOCKER_IMAGE=$(DOCKER_REPO):$(VERSION)
 
 $( shell mkdir -p bin )
+$( shell mkdir -p _output/images )
+$( shell mkdir -p _output/bin )
 
 user=$(shell id -u -n)
 group=$(shell id -g -n)
@@ -56,8 +58,12 @@ lint:
       golint -set_exit_status $$package $$i || exit 1; \
 	done
 
+_output/bin/dex:
+	@./scripts/docker-build
+	@sudo chown $(user):$(group) _output/bin/dex
+
 .PHONY: docker-image
-docker-image:
+docker-image: clean-release _output/bin/dex
 	@sudo docker build -t $(DOCKER_IMAGE) .
 
 .PHONY: proto
@@ -79,9 +85,13 @@ bin/protoc-gen-go:
 check-go-version:
 	@./scripts/check-go-version
 
-clean:
+clean: clean-release
 	@rm -rf bin/
 
+.PHONY: clean-release
+clean-release:
+	@rm -rf _output/
+
 testall: testrace vet fmt lint
 
 FORCE:
diff --git a/README.md b/README.md
index 61358c53..1474b17a 100644
--- a/README.md
+++ b/README.md
@@ -67,8 +67,6 @@ More docs for running dex as a Kubernetes authenticator can be found [here](Docu
   * [GitLab](Documentation/gitlab-connector.md)
   * [SAML 2.0](Documentation/saml-connector.md)
   * [OpenID Connect](Documentation/oidc-connector.md) (includes Google, Salesforce, Azure, etc.)
-  * [authproxy](Documentation/authproxy.md) (Apache2 mod_auth, etc.)
-  * [LinkedIn](Documentation/linkedin-connector.md)
 * Client libraries
   * [Go][go-oidc]
 
diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go
index 1a4a6c82..86d20603 100644
--- a/cmd/dex/serve.go
+++ b/cmd/dex/serve.go
@@ -75,7 +75,7 @@ func serve(cmd *cobra.Command, args []string) error {
 	}{
 		{c.Issuer == "", "no issuer specified in config file"},
 		{!c.EnablePasswordDB && len(c.StaticPasswords) != 0, "cannot specify static passwords without enabling password db"},
-		{c.Storage.Config == nil, "no storage supplied in config file"},
+		{c.Storage.Config == nil, "no storage suppied in config file"},
 		{c.Web.HTTP == "" && c.Web.HTTPS == "", "must supply a HTTP/HTTPS  address to listen on"},
 		{c.Web.HTTPS != "" && c.Web.TLSCert == "", "no cert specified for HTTPS"},
 		{c.Web.HTTPS != "" && c.Web.TLSKey == "", "no private key specified for HTTPS"},
diff --git a/connector/authproxy/authproxy.go b/connector/authproxy/authproxy.go
deleted file mode 100644
index e367c4f9..00000000
--- a/connector/authproxy/authproxy.go
+++ /dev/null
@@ -1,57 +0,0 @@
-// Package authproxy implements a connector which relies on external
-// authentication (e.g. mod_auth in Apache2) and returns an identity with the
-// HTTP header X-Remote-User as verified email.
-package authproxy
-
-import (
-	"fmt"
-	"net/http"
-	"net/url"
-
-	"github.com/coreos/dex/connector"
-	"github.com/sirupsen/logrus"
-)
-
-// Config holds the configuration parameters for a connector which returns an
-// identity with the HTTP header X-Remote-User as verified email.
-type Config struct{}
-
-// Open returns an authentication strategy which requires no user interaction.
-func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
-	return &callback{logger: logger, pathSuffix: "/" + id}, nil
-}
-
-// Callback is a connector which returns an identity with the HTTP header
-// X-Remote-User as verified email.
-type callback struct {
-	logger     logrus.FieldLogger
-	pathSuffix string
-}
-
-// LoginURL returns the URL to redirect the user to login with.
-func (m *callback) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) {
-	u, err := url.Parse(callbackURL)
-	if err != nil {
-		return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err)
-	}
-	u.Path = u.Path + m.pathSuffix
-	v := u.Query()
-	v.Set("state", state)
-	u.RawQuery = v.Encode()
-	return u.String(), nil
-}
-
-// HandleCallback parses the request and returns the user's identity
-func (m *callback) HandleCallback(s connector.Scopes, r *http.Request) (connector.Identity, error) {
-	remoteUser := r.Header.Get("X-Remote-User")
-	if remoteUser == "" {
-		return connector.Identity{}, fmt.Errorf("required HTTP header X-Remote-User is not set")
-	}
-	// TODO: add support for X-Remote-Group, see
-	// https://kubernetes.io/docs/admin/authentication/#authenticating-proxy
-	return connector.Identity{
-		UserID:        remoteUser, // TODO: figure out if this is a bad ID value.
-		Email:         remoteUser,
-		EmailVerified: true,
-	}, nil
-}
diff --git a/connector/github/github.go b/connector/github/github.go
index fc578aeb..ae078412 100644
--- a/connector/github/github.go
+++ b/connector/github/github.go
@@ -63,7 +63,7 @@ type Org struct {
 }
 
 // Open returns a strategy for logging in through GitHub.
-func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
+func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) {
 
 	if c.Org != "" {
 		// Return error if both 'org' and 'orgs' fields are used.
diff --git a/connector/gitlab/gitlab.go b/connector/gitlab/gitlab.go
index f6cbbf09..60d2cdc2 100644
--- a/connector/gitlab/gitlab.go
+++ b/connector/gitlab/gitlab.go
@@ -22,11 +22,6 @@ const (
 	scopeAPI  = "api"
 )
 
-var (
-	reNext = regexp.MustCompile("<([^>]+)>; rel=\"next\"")
-	reLast = regexp.MustCompile("<([^>]+)>; rel=\"last\"")
-)
-
 // Config holds configuration options for gilab logins.
 type Config struct {
 	BaseURL      string `json:"baseURL"`
@@ -51,7 +46,7 @@ type gitlabGroup struct {
 }
 
 // Open returns a strategy for logging in through GitLab.
-func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
+func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) {
 	if c.BaseURL == "" {
 		c.BaseURL = "https://www.gitlab.com"
 	}
@@ -241,6 +236,9 @@ func (c *gitlabConnector) groups(ctx context.Context, client *http.Client) ([]st
 
 	apiURL := c.baseURL + "/api/v4/groups"
 
+	reNext := regexp.MustCompile("<(.*)>; rel=\"next\"")
+	reLast := regexp.MustCompile("<(.*)>; rel=\"last\"")
+
 	groups := []string{}
 	var gitlabGroups []gitlabGroup
 	for {
@@ -274,28 +272,22 @@ func (c *gitlabConnector) groups(ctx context.Context, client *http.Client) ([]st
 
 		link := resp.Header.Get("Link")
 
-		apiURL = nextURL(apiURL, link)
-		if apiURL == "" {
+		if len(reLast.FindStringSubmatch(link)) > 1 {
+			lastPageURL := reLast.FindStringSubmatch(link)[1]
+
+			if apiURL == lastPageURL {
+				break
+			}
+		} else {
 			break
 		}
-	}
-	return groups, nil
-}
 
-func nextURL(url string, link string) string {
-	if len(reLast.FindStringSubmatch(link)) > 1 {
-		lastPageURL := reLast.FindStringSubmatch(link)[1]
-
-		if url == lastPageURL {
-			return ""
+		if len(reNext.FindStringSubmatch(link)) > 1 {
+			apiURL = reNext.FindStringSubmatch(link)[1]
+		} else {
+			break
 		}
-	} else {
-		return ""
-	}
 
-	if len(reNext.FindStringSubmatch(link)) > 1 {
-		return reNext.FindStringSubmatch(link)[1]
 	}
-
-	return ""
+	return groups, nil
 }
diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go
index 5d19a51d..01125ae7 100644
--- a/connector/ldap/ldap.go
+++ b/connector/ldap/ldap.go
@@ -153,7 +153,7 @@ func parseScope(s string) (int, bool) {
 }
 
 // Open returns an authentication strategy using LDAP.
-func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
+func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) {
 	conn, err := c.OpenConnector(logger)
 	if err != nil {
 		return nil, err
diff --git a/connector/linkedin/linkedin.go b/connector/linkedin/linkedin.go
deleted file mode 100644
index ba85eefc..00000000
--- a/connector/linkedin/linkedin.go
+++ /dev/null
@@ -1,197 +0,0 @@
-// Package linkedin provides authentication strategies using LinkedIn
-package linkedin
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"net/http"
-	"strings"
-
-	"golang.org/x/oauth2"
-
-	"github.com/coreos/dex/connector"
-	"github.com/sirupsen/logrus"
-)
-
-const (
-	apiURL   = "https://api.linkedin.com/v1"
-	authURL  = "https://www.linkedin.com/oauth/v2/authorization"
-	tokenURL = "https://www.linkedin.com/oauth/v2/accessToken"
-)
-
-// Config holds configuration options for LinkedIn logins.
-type Config struct {
-	ClientID     string `json:"clientID"`
-	ClientSecret string `json:"clientSecret"`
-	RedirectURI  string `json:"redirectURI"`
-}
-
-// Open returns a strategy for logging in through LinkedIn
-func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
-	return &linkedInConnector{
-		oauth2Config: &oauth2.Config{
-			ClientID:     c.ClientID,
-			ClientSecret: c.ClientSecret,
-			Endpoint: oauth2.Endpoint{
-				AuthURL:  authURL,
-				TokenURL: tokenURL,
-			},
-			Scopes:      []string{"r_basicprofile", "r_emailaddress"},
-			RedirectURL: c.RedirectURI,
-		},
-		logger: logger,
-	}, nil
-}
-
-type connectorData struct {
-	AccessToken string `json:"accessToken"`
-}
-
-type linkedInConnector struct {
-	oauth2Config *oauth2.Config
-	logger       logrus.FieldLogger
-}
-
-// LinkedIn doesn't provide refresh tokens, so refresh tokens issued by Dex
-// will expire in 60 days (default LinkedIn token lifetime).
-var (
-	_ connector.CallbackConnector = (*linkedInConnector)(nil)
-	_ connector.RefreshConnector  = (*linkedInConnector)(nil)
-)
-
-// LoginURL returns an access token request URL
-func (c *linkedInConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) {
-	if c.oauth2Config.RedirectURL != callbackURL {
-		return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q",
-			callbackURL, c.oauth2Config.RedirectURL)
-	}
-
-	return c.oauth2Config.AuthCodeURL(state), nil
-}
-
-// HandleCallback handles HTTP redirect from LinkedIn
-func (c *linkedInConnector) 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")}
-	}
-
-	ctx := r.Context()
-	token, err := c.oauth2Config.Exchange(ctx, q.Get("code"))
-	if err != nil {
-		return identity, fmt.Errorf("linkedin: get token: %v", err)
-	}
-
-	client := c.oauth2Config.Client(ctx, token)
-	profile, err := c.profile(ctx, client)
-	if err != nil {
-		return identity, fmt.Errorf("linkedin: get profile: %v", err)
-	}
-
-	identity = connector.Identity{
-		UserID:        profile.ID,
-		Username:      profile.fullname(),
-		Email:         profile.Email,
-		EmailVerified: true,
-	}
-
-	if s.OfflineAccess {
-		data := connectorData{AccessToken: token.AccessToken}
-		connData, err := json.Marshal(data)
-		if err != nil {
-			return identity, fmt.Errorf("linkedin: marshal connector data: %v", err)
-		}
-		identity.ConnectorData = connData
-	}
-
-	return identity, nil
-}
-
-func (c *linkedInConnector) Refresh(ctx context.Context, s connector.Scopes, ident connector.Identity) (connector.Identity, error) {
-	if len(ident.ConnectorData) == 0 {
-		return ident, fmt.Errorf("linkedin: no upstream access token found")
-	}
-
-	var data connectorData
-	if err := json.Unmarshal(ident.ConnectorData, &data); err != nil {
-		return ident, fmt.Errorf("linkedin: unmarshal access token: %v", err)
-	}
-
-	client := c.oauth2Config.Client(ctx, &oauth2.Token{AccessToken: data.AccessToken})
-	profile, err := c.profile(ctx, client)
-	if err != nil {
-		return ident, fmt.Errorf("linkedin: get profile: %v", err)
-	}
-
-	ident.Username = profile.fullname()
-	ident.Email = profile.Email
-
-	return ident, nil
-}
-
-type profile struct {
-	ID        string `json:"id"`
-	FirstName string `json:"firstName"`
-	LastName  string `json:"lastName"`
-	Email     string `json:"emailAddress"`
-}
-
-// fullname returns a full name of a person, or email if the resulting name is
-// empty
-func (p profile) fullname() string {
-	fname := strings.TrimSpace(p.FirstName + " " + p.LastName)
-	if fname == "" {
-		return p.Email
-	}
-
-	return fname
-}
-
-func (c *linkedInConnector) profile(ctx context.Context, client *http.Client) (p profile, err error) {
-	// https://developer.linkedin.com/docs/fields/basic-profile
-	req, err := http.NewRequest("GET", apiURL+"/people/~:(id,first-name,last-name,email-address)", nil)
-	if err != nil {
-		return p, fmt.Errorf("new req: %v", err)
-	}
-	q := req.URL.Query()
-	q.Add("format", "json")
-	req.URL.RawQuery = q.Encode()
-
-	resp, err := client.Do(req.WithContext(ctx))
-	if err != nil {
-		return p, fmt.Errorf("get URL %v", err)
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode != http.StatusOK {
-		body, err := ioutil.ReadAll(resp.Body)
-		if err != nil {
-			return p, fmt.Errorf("read body: %v", err)
-		}
-		return p, fmt.Errorf("%s: %s", resp.Status, body)
-	}
-
-	if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
-		return p, fmt.Errorf("JSON decode: %v", err)
-	}
-
-	if p.Email == "" {
-		return p, fmt.Errorf("email is not set")
-	}
-
-	return p, err
-}
-
-type oauth2Error struct {
-	error            string
-	errorDescription string
-}
-
-func (e *oauth2Error) Error() string {
-	if e.errorDescription == "" {
-		return e.error
-	}
-	return e.error + ": " + e.errorDescription
-}
diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go
index f0d8daf7..9cf7a3be 100644
--- a/connector/oidc/oidc.go
+++ b/connector/oidc/oidc.go
@@ -75,7 +75,7 @@ func registerBrokenAuthHeaderProvider(url string) {
 
 // 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) {
+func (c *Config) Open(logger logrus.FieldLogger) (conn connector.Connector, err error) {
 	ctx, cancel := context.WithCancel(context.Background())
 
 	provider, err := oidc.NewProvider(ctx, c.Issuer)
diff --git a/connector/saml/saml.go b/connector/saml/saml.go
index 02b3fad7..f35f0a81 100644
--- a/connector/saml/saml.go
+++ b/connector/saml/saml.go
@@ -125,7 +125,7 @@ func (c certStore) Certificates() (roots []*x509.Certificate, err error) {
 
 // Open validates the config and returns a connector. It does not actually
 // validate connectivity with the provider.
-func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
+func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) {
 	return c.openConnector(logger)
 }
 
diff --git a/scripts/docker-build b/scripts/docker-build
new file mode 100755
index 00000000..8195410f
--- /dev/null
+++ b/scripts/docker-build
@@ -0,0 +1,15 @@
+#!/bin/bash -e
+
+mkdir -p _output/bin
+
+sudo docker run \
+    --cidfile=cid \
+    -v $PWD:/go/src/github.com/coreos/dex:ro \
+    -w /go/src/github.com/coreos/dex \
+    golang:1.8.3-alpine \
+    /bin/sh -x -c \
+    'apk add --no-cache --update alpine-sdk && make release-binary'
+
+sudo docker cp $( cat cid ):/go/bin/dex _output/bin/dex
+sudo docker rm $( cat cid )
+sudo rm cid
diff --git a/server/handlers.go b/server/handlers.go
index 345cd496..a70858a9 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -346,12 +346,6 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	if connID := mux.Vars(r)["connector"]; connID != "" && connID != authReq.ConnectorID {
-		s.logger.Errorf("Connector mismatch: authentication started with id %q, but callback for id %q was triggered", authReq.ConnectorID, connID)
-		s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.")
-		return
-	}
-
 	conn, err := s.getConnector(authReq.ConnectorID)
 	if err != nil {
 		s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
diff --git a/server/oauth2.go b/server/oauth2.go
index 6a0d5eee..528b25a6 100644
--- a/server/oauth2.go
+++ b/server/oauth2.go
@@ -222,15 +222,6 @@ func accessTokenHash(alg jose.SignatureAlgorithm, accessToken string) (string, e
 
 type audience []string
 
-func (a audience) contains(aud string) bool {
-	for _, e := range a {
-		if aud == e {
-			return true
-		}
-	}
-	return false
-}
-
 func (a audience) MarshalJSON() ([]byte, error) {
 	if len(a) == 1 {
 		return json.Marshal(a[0])
@@ -337,13 +328,8 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
 		// client as the audience.
 		tok.Audience = audience{clientID}
 	} else {
-		// Client asked for cross client audience:
-		// if the current client was not requested explicitly
-		if !tok.Audience.contains(clientID) {
-			// by default it becomes one of entries in Audience
-			tok.Audience = append(tok.Audience, clientID)
-		}
-		// The current client becomes the authorizing party.
+		// Client asked for cross client audience. The current client
+		// becomes the authorizing party.
 		tok.AuthorizingParty = clientID
 	}
 
diff --git a/server/server.go b/server/server.go
index e0b7d359..aabe7abe 100644
--- a/server/server.go
+++ b/server/server.go
@@ -8,7 +8,6 @@ import (
 	"net/http"
 	"net/url"
 	"path"
-	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -20,11 +19,9 @@ import (
 	"github.com/sirupsen/logrus"
 
 	"github.com/coreos/dex/connector"
-	"github.com/coreos/dex/connector/authproxy"
 	"github.com/coreos/dex/connector/github"
 	"github.com/coreos/dex/connector/gitlab"
 	"github.com/coreos/dex/connector/ldap"
-	"github.com/coreos/dex/connector/linkedin"
 	"github.com/coreos/dex/connector/mock"
 	"github.com/coreos/dex/connector/oidc"
 	"github.com/coreos/dex/connector/saml"
@@ -242,19 +239,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
 	handleWithCORS("/keys", s.handlePublicKeys)
 	handleFunc("/auth", s.handleAuthorization)
 	handleFunc("/auth/{connector}", s.handleConnectorLogin)
-	r.HandleFunc(path.Join(issuerURL.Path, "/callback"), func(w http.ResponseWriter, r *http.Request) {
-		// Strip the X-Remote-* headers to prevent security issues on
-		// misconfigured authproxy connector setups.
-		for key := range r.Header {
-			if strings.HasPrefix(strings.ToLower(key), "x-remote-") {
-				r.Header.Del(key)
-			}
-		}
-		s.handleConnectorCallback(w, r)
-	})
-	// For easier connector-specific web server configuration, e.g. for the
-	// "authproxy" connector.
-	handleFunc("/callback/{connector}", s.handleConnectorCallback)
+	handleFunc("/callback", s.handleConnectorCallback)
 	handleFunc("/approval", s.handleApproval)
 	handleFunc("/healthz", s.handleHealth)
 	handlePrefix("/static", static)
@@ -396,7 +381,7 @@ func (s *Server) startGarbageCollection(ctx context.Context, frequency time.Dura
 
 // ConnectorConfig is a configuration that can open a connector.
 type ConnectorConfig interface {
-	Open(id string, logger logrus.FieldLogger) (connector.Connector, error)
+	Open(logrus.FieldLogger) (connector.Connector, error)
 }
 
 // ConnectorsConfig variable provides an easy way to return a config struct
@@ -409,8 +394,6 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{
 	"gitlab":       func() ConnectorConfig { return new(gitlab.Config) },
 	"oidc":         func() ConnectorConfig { return new(oidc.Config) },
 	"saml":         func() ConnectorConfig { return new(saml.Config) },
-	"authproxy":    func() ConnectorConfig { return new(authproxy.Config) },
-	"linkedin":     func() ConnectorConfig { return new(linkedin.Config) },
 	// Keep around for backwards compatibility.
 	"samlExperimental": func() ConnectorConfig { return new(saml.Config) },
 }
@@ -432,7 +415,7 @@ func openConnector(logger logrus.FieldLogger, conn storage.Connector) (connector
 		}
 	}
 
-	c, err := connConfig.Open(conn.ID, logger)
+	c, err := connConfig.Open(logger)
 	if err != nil {
 		return c, fmt.Errorf("failed to create connector %s: %v", conn.ID, err)
 	}
diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go
index ef4870d4..6eb4e258 100644
--- a/storage/kubernetes/client.go
+++ b/storage/kubernetes/client.go
@@ -157,7 +157,11 @@ func closeResp(r *http.Response) {
 }
 
 func (c *client) get(resource, name string, v interface{}) error {
-	url := c.urlFor(c.apiVersion, c.namespace, resource, name)
+	return c.getResource(c.apiVersion, c.namespace, resource, name, v)
+}
+
+func (c *client) getResource(apiVersion, namespace, resource, name string, v interface{}) error {
+	url := c.urlFor(apiVersion, namespace, resource, name)
 	resp, err := c.client.Get(url)
 	if err != nil {
 		return err
@@ -333,10 +337,7 @@ func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, l
 
 	logger.Infof("kubernetes client apiVersion = %s", apiVersion)
 	return &client{
-		client: &http.Client{
-			Transport: t,
-			Timeout:   15 * time.Second,
-		},
+		client:     &http.Client{Transport: t},
 		baseURL:    cluster.Server,
 		hash:       func() hash.Hash { return fnv.New64() },
 		namespace:  namespace,
diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go
index 1cb0fd07..6acf89a0 100644
--- a/storage/kubernetes/storage.go
+++ b/storage/kubernetes/storage.go
@@ -53,9 +53,9 @@ func (c *Config) Open(logger logrus.FieldLogger) (storage.Storage, error) {
 // open returns a kubernetes client, initializing the third party resources used
 // by dex.
 //
-// errOnResources controls if errors creating the resources cause this method to return
+// waitForResources controls if errors creating the resources cause this method to return
 // immediately (used during testing), or if the client will asynchronously retry.
-func (c *Config) open(logger logrus.FieldLogger, errOnResources bool) (*client, error) {
+func (c *Config) open(logger logrus.FieldLogger, waitForResources bool) (*client, error) {
 	if c.InCluster && (c.KubeConfigFile != "") {
 		return nil, errors.New("cannot specify both 'inCluster' and 'kubeConfigFile'")
 	}
@@ -85,9 +85,8 @@ func (c *Config) open(logger logrus.FieldLogger, errOnResources bool) (*client,
 
 	ctx, cancel := context.WithCancel(context.Background())
 
-	logger.Info("creating custom Kubernetes resources")
 	if !cli.registerCustomResources(c.UseTPR) {
-		if errOnResources {
+		if waitForResources {
 			cancel()
 			return nil, fmt.Errorf("failed creating custom resources")
 		}
@@ -111,6 +110,13 @@ func (c *Config) open(logger logrus.FieldLogger, errOnResources bool) (*client,
 		}()
 	}
 
+	if waitForResources {
+		if err := cli.waitForCRDs(ctx); err != nil {
+			cancel()
+			return nil, err
+		}
+	}
+
 	// If the client is closed, stop trying to create resources.
 	cli.cancel = cancel
 	return cli, nil
@@ -123,9 +129,6 @@ func (c *Config) open(logger logrus.FieldLogger, errOnResources bool) (*client,
 // It logs all errors, returning true if the resources were created successfully.
 //
 // Creating a custom resource does not mean that they'll be immediately available.
-//
-// TODO(ericchiang): Provide an option to wait for the resources to actually
-// be available.
 func (cli *client) registerCustomResources(useTPR bool) (ok bool) {
 	ok = true
 	length := len(customResourceDefinitions)
@@ -165,6 +168,49 @@ func (cli *client) registerCustomResources(useTPR bool) (ok bool) {
 	return ok
 }
 
+// waitForCRDs waits for all CRDs to be in a ready state, and is used
+// by the tests to synchronize before running conformance.
+func (cli *client) waitForCRDs(ctx context.Context) error {
+	ctx, cancel := context.WithTimeout(ctx, time.Second*30)
+	defer cancel()
+
+	for _, crd := range customResourceDefinitions {
+		for {
+			err := cli.isCRDReady(crd.Name)
+			if err == nil {
+				break
+			}
+
+			cli.logger.Errorf("checking CRD: %v", err)
+
+			select {
+			case <-ctx.Done():
+				return errors.New("timed out waiting for CRDs to be available")
+			case <-time.After(time.Millisecond * 100):
+			}
+		}
+	}
+	return nil
+}
+
+// isCRDReady determines if a CRD is ready by inspecting its conditions.
+func (cli *client) isCRDReady(name string) error {
+	var r k8sapi.CustomResourceDefinition
+	err := cli.getResource("apiextensions.k8s.io/v1beta1", "", "customresourcedefinitions", name, &r)
+	if err != nil {
+		return fmt.Errorf("get crd %s: %v", name, err)
+	}
+
+	conds := make(map[string]string) // For debugging, keep the conditions around.
+	for _, c := range r.Status.Conditions {
+		if c.Type == k8sapi.Established && c.Status == k8sapi.ConditionTrue {
+			return nil
+		}
+		conds[string(c.Type)] = string(c.Status)
+	}
+	return fmt.Errorf("crd %s not ready %#v", name, conds)
+}
+
 func (cli *client) Close() error {
 	if cli.cancel != nil {
 		cli.cancel()
diff --git a/web/static/img/linkedin-icon.svg b/web/static/img/linkedin-icon.svg
deleted file mode 100644
index 409bad5e..00000000
--- a/web/static/img/linkedin-icon.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<?xml version="1.0" ?><svg style="enable-background:new 0 0 48 48;" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Icons"><g id="Icons_3_"><g><path d="M34.9549,10.525h-21.91c-1.3899,0-2.5199,1.13-2.5199,2.52v21.91c0,1.39,1.13,2.52,2.5199,2.52     h21.91c1.3901,0,2.5201-1.13,2.5201-2.52V13.0449C37.475,11.655,36.345,10.525,34.9549,10.525z M19.415,31.335     c-0.95-0.01-1.9101-0.01-2.8701,0c-0.18,0-0.23-0.05-0.23-0.24v-10.14c0-0.19,0.06-0.24,0.24-0.24     c0.95,0.01,1.9101,0.01,2.8601,0c0.1899,0,0.24,0.06,0.23,0.24v5.08c0,1.69,0,3.3799,0.01,5.07     C19.655,31.285,19.595,31.335,19.415,31.335z M17.9849,19.3549c-1.06,0.0101-1.93-0.8799-1.93-1.9299     c0.01-1.0101,0.7801-1.92,1.9201-1.93c1.0799-0.01,1.94,0.81,1.94,1.93C19.915,18.525,19.0749,19.345,17.9849,19.3549z      M31.645,31.335h-2.81c-0.2301,0-0.24-0.01-0.24-0.25v-5.14c0-0.48-0.01-0.96-0.1501-1.43c-0.2199-0.73-0.69-1.08-1.46-1.1001     c-0.54-0.0099-1.05,0.0901-1.43,0.5101c-0.31,0.33-0.44,0.73-0.5,1.17c-0.0399,0.3899-0.07,0.79-0.07,1.19     c-0.0099,1.61-0.0099,3.22,0,4.8199c0,0.1801-0.05,0.2301-0.2199,0.2301c-0.9601-0.01-1.93-0.01-2.8901,0     c-0.1699,0-0.2299-0.05-0.2299-0.2301c0.01-3.3799,0.01-6.7699,0-10.1599c0-0.18,0.07-0.23,0.2299-0.23     c0.92,0.01,1.8401,0.01,2.7601,0c0.1799,0,0.2399,0.07,0.2299,0.24c-0.01,0.37,0,0.75,0,1.12     c0.5201-0.77,1.2201-1.26,2.1001-1.48c1.0199-0.25,2.0299-0.17,2.99,0.25c0.93,0.4,1.43,1.16,1.69,2.1     c0.18,0.61,0.26,1.24,0.27,1.88c0.0099,2.07,0.0199,4.14,0.03,6.22C31.9449,31.335,31.9449,31.335,31.645,31.335z" style="fill:#0097D3;"/></g></g></g></svg>
\ No newline at end of file
diff --git a/web/static/main.css b/web/static/main.css
index a73b591f..9df2052f 100644
--- a/web/static/main.css
+++ b/web/static/main.css
@@ -83,11 +83,6 @@ body {
   background-image: url(../static/img/saml-icon.svg);
 }
 
-.dex-btn-icon--linkedin {
-  background-image: url(../static/img/linkedin-icon.svg);
-  background-size: contain;
-}
-
 .dex-btn-text {
   font-weight: 600;
   line-height: 36px;
diff --git a/web/templates/password.html b/web/templates/password.html
index 7a6c8aa6..e13363ef 100644
--- a/web/templates/password.html
+++ b/web/templates/password.html
@@ -17,12 +17,12 @@
     </div>
 
     {{ if .Invalid }}
-      <div id="login-error" class="dex-error-box">
+      <div class="dex-error-box">
         Invalid username and password.
       </div>
     {{ end }}
 
-    <button tabindex="3" id="submit-login" type="submit" class="dex-btn theme-btn--primary">Login</button>
+    <button tabindex="3" type="submit" class="dex-btn theme-btn--primary">Login</button>
 
   </form>
 </div>


Test patch:
diff --git a/Documentation/dev-integration-tests.md b/Documentation/dev-integration-tests.md
index 366b4ae3..a9ce1ad3 100644
--- a/Documentation/dev-integration-tests.md
+++ b/Documentation/dev-integration-tests.md
@@ -2,7 +2,7 @@
 
 ## Kubernetes
 
-Kubernetes tests will only run if the `DEX_KUBECONFIG` environment variable is set.
+Kubernetes tests run against a Kubernetes API server, and are enabled by the `DEX_KUBECONFIG` environment variable:
 
 ```
 $ export DEX_KUBECONFIG=~/.kube/config
@@ -10,7 +10,11 @@ $ go test -v -i ./storage/kubernetes
 $ go test -v ./storage/kubernetes
 ```
 
-Because third party resources creation isn't synchronized it's expected that the tests fail the first time. Fear not, and just run them again.
+These tests can be executed locally using docker by running the following script:
+
+```
+$ ./scripts/test-k8s.sh
+```
 
 ## Postgres
 
diff --git a/connector/gitlab/gitlab_test.go b/connector/gitlab/gitlab_test.go
deleted file mode 100644
index ef669261..00000000
--- a/connector/gitlab/gitlab_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package gitlab
-
-import "testing"
-
-var nextURLTests = []struct {
-	link     string
-	expected string
-}{
-	{"<https://gitlab.com/api/v4/groups?page=2&per_page=20>; rel=\"next\", " +
-		"<https://gitlab.com/api/v4/groups?page=1&per_page=20>; rel=\"prev\"; pet=\"cat\", " +
-		"<https://gitlab.com/api/v4/groups?page=3&per_page=20>; rel=\"last\"",
-		"https://gitlab.com/api/v4/groups?page=2&per_page=20"},
-	{"<https://gitlab.com/api/v4/groups?page=3&per_page=20>; rel=\"next\", " +
-		"<https://gitlab.com/api/v4/groups?page=2&per_page=20>; rel=\"prev\"; pet=\"dog\", " +
-		"<https://gitlab.com/api/v4/groups?page=3&per_page=20>; rel=\"last\"",
-		"https://gitlab.com/api/v4/groups?page=3&per_page=20"},
-	{"<https://gitlab.com/api/v4/groups?page=3&per_page=20>; rel=\"prev\"; pet=\"bunny\", " +
-		"<https://gitlab.com/api/v4/groups?page=3&per_page=20>; rel=\"last\"",
-		""},
-}
-
-func TestNextURL(t *testing.T) {
-	apiURL := "https://gitlab.com/api/v4/groups"
-	for _, tt := range nextURLTests {
-		apiURL = nextURL(apiURL, tt.link)
-		if apiURL != tt.expected {
-			t.Errorf("Should have returned %s, got %s", tt.expected, apiURL)
-		}
-	}
-}
diff --git a/connector/ldap/ldap_test.go b/connector/ldap/ldap_test.go
index 3b903856..08f85be3 100644
--- a/connector/ldap/ldap_test.go
+++ b/connector/ldap/ldap_test.go
@@ -28,7 +28,6 @@ const (
 	connectStartTLS connectionMethod = iota
 	connectLDAPS
 	connectLDAP
-	connectInsecureSkipVerify
 )
 
 // subtest is a login test against a given schema.
@@ -351,49 +350,6 @@ userpassword: foo
 	runTests(t, schema, connectStartTLS, c, tests)
 }
 
-func TestInsecureSkipVerify(t *testing.T) {
-	schema := `
-dn: dc=example,dc=org
-objectClass: dcObject
-objectClass: organization
-o: Example Company
-dc: example
-
-dn: ou=People,dc=example,dc=org
-objectClass: organizationalUnit
-ou: People
-
-dn: cn=jane,ou=People,dc=example,dc=org
-objectClass: person
-objectClass: inetOrgPerson
-sn: doe
-cn: jane
-mail: janedoe@example.com
-userpassword: foo
-`
-	c := &Config{}
-	c.UserSearch.BaseDN = "ou=People,dc=example,dc=org"
-	c.UserSearch.NameAttr = "cn"
-	c.UserSearch.EmailAttr = "mail"
-	c.UserSearch.IDAttr = "DN"
-	c.UserSearch.Username = "cn"
-
-	tests := []subtest{
-		{
-			name:     "validpassword",
-			username: "jane",
-			password: "foo",
-			want: connector.Identity{
-				UserID:        "cn=jane,ou=People,dc=example,dc=org",
-				Username:      "jane",
-				Email:         "janedoe@example.com",
-				EmailVerified: true,
-			},
-		},
-	}
-	runTests(t, schema, connectInsecureSkipVerify, c, tests)
-}
-
 func TestLDAPS(t *testing.T) {
 	schema := `
 dn: dc=example,dc=org
@@ -575,9 +531,6 @@ func runTests(t *testing.T, schema string, connMethod connectionMethod, config *
 	case connectLDAPS:
 		c.Host = "localhost:10636"
 		c.RootCA = "testdata/ca.crt"
-	case connectInsecureSkipVerify:
-		c.Host = "localhost:10636"
-		c.InsecureSkipVerify = true
 	case connectLDAP:
 		c.Host = "localhost:10389"
 		c.InsecureNoSSL = true
diff --git a/connector/mock/connectortest.go b/connector/mock/connectortest.go
index 18abd820..7236230a 100644
--- a/connector/mock/connectortest.go
+++ b/connector/mock/connectortest.go
@@ -69,7 +69,7 @@ func (m *Callback) Refresh(ctx context.Context, s connector.Scopes, identity con
 type CallbackConfig struct{}
 
 // Open returns an authentication strategy which requires no user interaction.
-func (c *CallbackConfig) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
+func (c *CallbackConfig) Open(logger logrus.FieldLogger) (connector.Connector, error) {
 	return NewCallbackConnector(logger), nil
 }
 
@@ -81,7 +81,7 @@ type PasswordConfig struct {
 }
 
 // Open returns an authentication strategy which prompts for a predefined username and password.
-func (c *PasswordConfig) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
+func (c *PasswordConfig) Open(logger logrus.FieldLogger) (connector.Connector, error) {
 	if c.Username == "" {
 		return nil, errors.New("no username supplied")
 	}
diff --git a/connector/oidc/oidc_test.go b/connector/oidc/oidc_test.go
index 76a2b5e2..305ab2ce 100644
--- a/connector/oidc/oidc_test.go
+++ b/connector/oidc/oidc_test.go
@@ -1,6 +1,11 @@
 package oidc
 
 import (
+	"github.com/coreos/dex/connector"
+	"github.com/sirupsen/logrus"
+	"net/url"
+	"os"
+	"reflect"
 	"testing"
 )
 
@@ -23,3 +28,95 @@ func TestKnownBrokenAuthHeaderProvider(t *testing.T) {
 		}
 	}
 }
+
+func TestOidcConnector_LoginURL(t *testing.T) {
+	logger := &logrus.Logger{
+		Out:       os.Stderr,
+		Formatter: &logrus.TextFormatter{DisableColors: true},
+		Level:     logrus.DebugLevel,
+	}
+
+	tests := []struct {
+		scopes        connector.Scopes
+		hostedDomains []string
+
+		wantScopes  string
+		wantHdParam string
+	}{
+		{
+			connector.Scopes{}, []string{"example.com"},
+			"openid profile email", "example.com",
+		},
+		{
+			connector.Scopes{}, []string{"mydomain.org", "example.com"},
+			"openid profile email", "*",
+		},
+		{
+			connector.Scopes{}, []string{},
+			"openid profile email", "",
+		},
+		{
+			connector.Scopes{OfflineAccess: true}, []string{},
+			"openid profile email", "",
+		},
+	}
+
+	callback := "https://dex.example.com/callback"
+	state := "secret"
+
+	for _, test := range tests {
+		config := &Config{
+			Issuer:        "https://accounts.google.com",
+			ClientID:      "client-id",
+			ClientSecret:  "client-secret",
+			RedirectURI:   "https://dex.example.com/callback",
+			HostedDomains: test.hostedDomains,
+		}
+
+		conn, err := config.Open(logger)
+		if err != nil {
+			t.Errorf("failed to open connector: %v", err)
+			continue
+		}
+
+		loginURL, err := conn.(connector.CallbackConnector).LoginURL(test.scopes, callback, state)
+		if err != nil {
+			t.Errorf("failed to get login URL: %v", err)
+			continue
+		}
+
+		actual, err := url.Parse(loginURL)
+		if err != nil {
+			t.Errorf("failed to parse login URL: %v", err)
+			continue
+		}
+
+		wanted, _ := url.Parse("https://accounts.google.com/o/oauth2/v2/auth")
+		wantedQuery := &url.Values{}
+		wantedQuery.Set("client_id", config.ClientID)
+		wantedQuery.Set("redirect_uri", config.RedirectURI)
+		wantedQuery.Set("response_type", "code")
+		wantedQuery.Set("state", "secret")
+		wantedQuery.Set("scope", test.wantScopes)
+		if test.wantHdParam != "" {
+			wantedQuery.Set("hd", test.wantHdParam)
+		}
+		wanted.RawQuery = wantedQuery.Encode()
+
+		if !reflect.DeepEqual(actual, wanted) {
+			t.Errorf("Wanted %v, got %v", wanted, actual)
+		}
+	}
+}
+
+//func TestOidcConnector_HandleCallback(t *testing.T) {
+//	logger := &logrus.Logger{
+//		Out:       os.Stderr,
+//		Formatter: &logrus.TextFormatter{DisableColors: true},
+//		Level:     logrus.DebugLevel,
+//	}
+//
+//	tests := []struct {
+//
+//	}
+//}
diff --git a/scripts/test-k8s.sh b/scripts/test-k8s.sh
new file mode 100755
index 00000000..86127be1
--- /dev/null
+++ b/scripts/test-k8s.sh
@@ -0,0 +1,56 @@
+#!/bin/bash -e
+
+TEMPDIR=$( mktemp -d )
+
+cat << EOF > $TEMPDIR/kubeconfig
+apiVersion: v1
+kind: Config
+clusters:
+- name: local
+  cluster:
+    server: http://localhost:8080
+users:
+- name: local
+  user:
+contexts:
+- context:
+    cluster: local
+    user: local
+EOF
+
+cleanup () {
+    docker rm -f $( cat $TEMPDIR/etcd )
+    docker rm -f $( cat $TEMPDIR/kube-apiserver )
+    rm -rf $TEMPDIR
+}
+
+trap "{ CODE=$?; cleanup ; exit $CODE; }" EXIT
+
+docker run \
+    --cidfile=$TEMPDIR/etcd \
+    -d \
+    --net=host \
+    gcr.io/google_containers/etcd:3.1.10 \
+    etcd
+
+docker run \
+    --cidfile=$TEMPDIR/kube-apiserver \
+    -d \
+    -v $TEMPDIR:/var/run/kube-test:ro \
+    --net=host \
+    gcr.io/google_containers/kube-apiserver-amd64:v1.7.4 \
+    kube-apiserver \
+    --etcd-servers=http://localhost:2379 \
+    --service-cluster-ip-range=10.0.0.1/16 \
+    --insecure-bind-address=0.0.0.0 \
+    --insecure-port=8080
+
+until $(curl --output /dev/null --silent --head --fail http://localhost:8080/healthz); do
+    printf '.'
+    sleep 1
+done
+echo "API server ready"
+
+export DEX_KUBECONFIG=$TEMPDIR/kubeconfig
+go test -v -i ./storage/kubernetes
+go test -v ./storage/kubernetes
diff --git a/server/server_test.go b/server/server_test.go
index a67cfbf4..88110087 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -812,127 +812,6 @@ func TestCrossClientScopes(t *testing.T) {
 	}
 }
 
-func TestCrossClientScopesWithAzpInAudienceByDefault(t *testing.T) {
-	ctx, cancel := context.WithCancel(context.Background())
-	defer cancel()
-
-	httpServer, s := newTestServer(ctx, t, func(c *Config) {
-		c.Issuer = c.Issuer + "/non-root-path"
-	})
-	defer httpServer.Close()
-
-	p, err := oidc.NewProvider(ctx, httpServer.URL)
-	if err != nil {
-		t.Fatalf("failed to get provider: %v", err)
-	}
-
-	var (
-		reqDump, respDump []byte
-		gotCode           bool
-		state             = "a_state"
-	)
-	defer func() {
-		if !gotCode {
-			t.Errorf("never got a code in callback\n%s\n%s", reqDump, respDump)
-		}
-	}()
-
-	testClientID := "testclient"
-	peerID := "peer"
-
-	var oauth2Config *oauth2.Config
-	oauth2Server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		if r.URL.Path == "/callback" {
-			q := r.URL.Query()
-			if errType := q.Get("error"); errType != "" {
-				if desc := q.Get("error_description"); desc != "" {
-					t.Errorf("got error from server %s: %s", errType, desc)
-				} else {
-					t.Errorf("got error from server %s", errType)
-				}
-				w.WriteHeader(http.StatusInternalServerError)
-				return
-			}
-
-			if code := q.Get("code"); code != "" {
-				gotCode = true
-				token, err := oauth2Config.Exchange(ctx, code)
-				if err != nil {
-					t.Errorf("failed to exchange code for token: %v", err)
-					return
-				}
-				rawIDToken, ok := token.Extra("id_token").(string)
-				if !ok {
-					t.Errorf("no id token found: %v", err)
-					return
-				}
-				idToken, err := p.Verifier(&oidc.Config{ClientID: testClientID}).Verify(ctx, rawIDToken)
-				if err != nil {
-					t.Errorf("failed to parse ID Token: %v", err)
-					return
-				}
-
-				sort.Strings(idToken.Audience)
-				expAudience := []string{peerID, testClientID}
-				if !reflect.DeepEqual(idToken.Audience, expAudience) {
-					t.Errorf("expected audience %q, got %q", expAudience, idToken.Audience)
-				}
-
-			}
-			if gotState := q.Get("state"); gotState != state {
-				t.Errorf("state did not match, want=%q got=%q", state, gotState)
-			}
-			w.WriteHeader(http.StatusOK)
-			return
-		}
-		http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusSeeOther)
-	}))
-
-	defer oauth2Server.Close()
-
-	redirectURL := oauth2Server.URL + "/callback"
-	client := storage.Client{
-		ID:           testClientID,
-		Secret:       "testclientsecret",
-		RedirectURIs: []string{redirectURL},
-	}
-	if err := s.storage.CreateClient(client); err != nil {
-		t.Fatalf("failed to create client: %v", err)
-	}
-
-	peer := storage.Client{
-		ID:           peerID,
-		Secret:       "foobar",
-		TrustedPeers: []string{"testclient"},
-	}
-
-	if err := s.storage.CreateClient(peer); err != nil {
-		t.Fatalf("failed to create client: %v", err)
-	}
-
-	oauth2Config = &oauth2.Config{
-		ClientID:     client.ID,
-		ClientSecret: client.Secret,
-		Endpoint:     p.Endpoint(),
-		Scopes: []string{
-			oidc.ScopeOpenID, "profile", "email",
-			"audience:server:client_id:" + peer.ID,
-		},
-		RedirectURL: redirectURL,
-	}
-
-	resp, err := http.Get(oauth2Server.URL + "/login")
-	if err != nil {
-		t.Fatalf("get failed: %v", err)
-	}
-	if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
-		t.Fatal(err)
-	}
-	if respDump, err = httputil.DumpResponse(resp, true); err != nil {
-		t.Fatal(err)
-	}
-}
-
 func TestPasswordDB(t *testing.T) {
 	s := memory.New(logger)
 	conn := newPasswordDB(s)