diff --git a/Documentation/connectors/openshift.md b/Documentation/connectors/openshift.md
new file mode 100644
index 0000000000000000000000000000000000000000..a33ca7721fceb6db06d37023530479974a41295c
--- /dev/null
+++ b/Documentation/connectors/openshift.md
@@ -0,0 +1,50 @@
+# Authentication using OpenShift
+
+## Overview
+
+Dex can make use of users and groups defined within OpenShift by querying the platform provided OAuth server.
+
+## Configuration
+
+Create a new OAuth Client by following the steps described in the documentation for [Registering Additional OAuth Clients[(https://docs.openshift.com/container-platform/latest/authentication/configuring-internal-oauth.html#oauth-register-additional-client_configuring-internal-oauth)
+
+This involves creating a resource similar the following
+
+```yaml
+kind: OAuthClient
+apiVersion: oauth.openshift.io/v1
+metadata:
+ name: dex
+# The value that should be utilized as the `client_secret`
+secret: "<clientSecret>" 
+# List of valid addresses for the callback. Ensure one of the values that are provided is `(dex issuer)/callback` 
+redirectURIs:
+ - "https:///<dex_url>/callback" 
+grantMethod: prompt
+```
+
+The following is an example of a configuration for `examples/config-dev.yaml`:
+
+```yaml
+connectors:
+  - type: openshift
+    # Required field for connector id.
+    id: openshift
+    # Required field for connector name.
+    name: OppenShift
+    config:
+      # OpenShift API
+      baseURL: https://api.mycluster.example.com:6443
+      # Credentials can be string literals or pulled from the environment.
+      clientID: $OPENSHIFT_OAUTH_CLIENT_ID
+      clientSecret: $OPENSHIFT_OAUTH_CLIENT_SECRET
+      redirectURI: http://127.0.0.1:5556/dex/
+      # Optional: Specify whether to communicate to OpenShift without validating SSL ceertificates
+      insecureCA: false
+      # Optional: The location of file containing SSL certificates to commmunicate to OpenShift
+      rootCA: /etc/ssl/openshift.pem
+      # Optional list of required groups a user mmust be a member of
+      groups:
+        - users
+
+```
\ No newline at end of file
diff --git a/connector/openshift/openshift.go b/connector/openshift/openshift.go
new file mode 100644
index 0000000000000000000000000000000000000000..69c8874104e0651c0be946ad0e975c83a7011427
--- /dev/null
+++ b/connector/openshift/openshift.go
@@ -0,0 +1,252 @@
+package openshift
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/dexidp/dex/connector"
+	"github.com/dexidp/dex/pkg/groups"
+	"github.com/dexidp/dex/pkg/log"
+	"github.com/dexidp/dex/storage/kubernetes/k8sapi"
+	"golang.org/x/oauth2"
+)
+
+// Config holds configuration options for OpenShift login
+type Config struct {
+	Issuer       string   `json:"issuer"`
+	ClientID     string   `json:"clientID"`
+	ClientSecret string   `json:"clientSecret"`
+	RedirectURI  string   `json:"redirectURI"`
+	Groups       []string `json:"groups"`
+	InsecureCA   bool     `json:"insecureCA"`
+	RootCA       string   `json:"rootCA"`
+}
+
+var (
+	_ connector.CallbackConnector = (*openshiftConnector)(nil)
+)
+
+type openshiftConnector struct {
+	apiURL       string
+	redirectURI  string
+	clientID     string
+	clientSecret string
+	cancel       context.CancelFunc
+	logger       log.Logger
+	httpClient   *http.Client
+	oauth2Config *oauth2.Config
+	insecureCA   bool
+	rootCA       string
+	groups       []string
+}
+
+type user struct {
+	k8sapi.TypeMeta   `json:",inline"`
+	k8sapi.ObjectMeta `json:"metadata,omitempty"`
+	Identities        []string `json:"identities" protobuf:"bytes,3,rep,name=identities"`
+	FullName          string   `json:"fullName,omitempty" protobuf:"bytes,2,opt,name=fullName"`
+	Groups            []string `json:"groups" protobuf:"bytes,4,rep,name=groups"`
+}
+
+// Open returns a connector which can be used to login users through an upstream
+// OpenShift OAuth2 provider.
+func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	wellKnownURL := strings.TrimSuffix(c.Issuer, "/") + "/.well-known/oauth-authorization-server"
+	req, err := http.NewRequest(http.MethodGet, wellKnownURL, nil)
+
+	openshiftConnector := openshiftConnector{
+		apiURL:       c.Issuer,
+		cancel:       cancel,
+		clientID:     c.ClientID,
+		clientSecret: c.ClientSecret,
+		insecureCA:   c.InsecureCA,
+		logger:       logger,
+		redirectURI:  c.RedirectURI,
+		rootCA:       c.RootCA,
+		groups:       c.Groups,
+	}
+
+	if openshiftConnector.httpClient, err = newHTTPClient(c.InsecureCA, c.RootCA); err != nil {
+		cancel()
+		return nil, fmt.Errorf("failed to create HTTP client: %v", err)
+	}
+
+	var metadata struct {
+		Auth  string `json:"authorization_endpoint"`
+		Token string `json:"token_endpoint"`
+	}
+
+	resp, err := openshiftConnector.httpClient.Do(req.WithContext(ctx))
+
+	if err != nil {
+		cancel()
+		return nil, fmt.Errorf("Failed to query OpenShift Endpoint %v", err)
+	}
+
+	defer resp.Body.Close()
+
+	if err := json.NewDecoder(resp.Body).Decode(&metadata); err != nil {
+		cancel()
+		return nil, fmt.Errorf("discovery through endpoint %s failed to decode body: %v",
+			wellKnownURL, err)
+	}
+
+	openshiftConnector.oauth2Config = &oauth2.Config{
+		ClientID:     c.ClientID,
+		ClientSecret: c.ClientSecret,
+		Endpoint: oauth2.Endpoint{
+			AuthURL: metadata.Auth, TokenURL: metadata.Token,
+		},
+		Scopes:      []string{"user:info", "user:check-access", "user:full"},
+		RedirectURL: c.RedirectURI,
+	}
+	return &openshiftConnector, nil
+}
+
+func (c *openshiftConnector) Close() error {
+	c.cancel()
+	return nil
+}
+
+// LoginURL returns the URL to redirect the user to login with.
+func (c *openshiftConnector) LoginURL(scopes 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)
+	}
+	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
+}
+
+// HandleCallback parses the request and returns the user's identity
+func (c *openshiftConnector) 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()
+	if c.httpClient != nil {
+		ctx = context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient)
+	}
+
+	token, err := c.oauth2Config.Exchange(ctx, q.Get("code"))
+	if err != nil {
+		return identity, fmt.Errorf("oidc: failed to get token: %v", err)
+	}
+
+	client := c.oauth2Config.Client(ctx, token)
+
+	user, err := c.user(ctx, client)
+
+	if err != nil {
+		return identity, fmt.Errorf("openshift: get user: %v", err)
+	}
+
+	validGroups := validateRequiredGroups(user.Groups, c.groups)
+
+	if !validGroups {
+		return identity, fmt.Errorf("openshift: user %q is not in any of the required teams", user.Name)
+	}
+
+	identity = connector.Identity{
+		UserID:            user.UID,
+		Username:          user.Name,
+		PreferredUsername: user.Name,
+		Groups:            user.Groups,
+	}
+
+	return identity, nil
+}
+
+// user function returns the OpenShift user associated with the authenticated user
+func (c *openshiftConnector) user(ctx context.Context, client *http.Client) (u user, err error) {
+	url := c.apiURL + "/apis/user.openshift.io/v1/users/~"
+
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return u, fmt.Errorf("new req: %v", err)
+	}
+
+	resp, err := client.Do(req.WithContext(ctx))
+	if err != nil {
+		return u, 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 u, fmt.Errorf("read body: %v", err)
+		}
+		return u, fmt.Errorf("%s: %s", resp.Status, body)
+	}
+
+	if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
+		return u, fmt.Errorf("JSON decode: %v", err)
+	}
+
+	return u, err
+}
+
+func validateRequiredGroups(userGroups, requiredGroups []string) bool {
+
+	matchingGroups := groups.Filter(userGroups, requiredGroups)
+
+	return len(requiredGroups) == len(matchingGroups)
+}
+
+// newHTTPClient returns a new HTTP client
+func newHTTPClient(insecureCA bool, rootCA string) (*http.Client, error) {
+	tlsConfig := tls.Config{}
+
+	if insecureCA {
+		tlsConfig = tls.Config{InsecureSkipVerify: true}
+	} else if rootCA != "" {
+		tlsConfig := tls.Config{RootCAs: x509.NewCertPool()}
+		rootCABytes, err := ioutil.ReadFile(rootCA)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read root-ca: %v", err)
+		}
+		if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) {
+			return nil, fmt.Errorf("no certs found in root CA file %q", rootCA)
+		}
+	}
+
+	return &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tlsConfig,
+			Proxy:           http.ProxyFromEnvironment,
+			DialContext: (&net.Dialer{
+				Timeout:   30 * time.Second,
+				KeepAlive: 30 * time.Second,
+				DualStack: true,
+			}).DialContext,
+			MaxIdleConns:          100,
+			IdleConnTimeout:       90 * time.Second,
+			TLSHandshakeTimeout:   10 * time.Second,
+			ExpectContinueTimeout: 1 * time.Second,
+		},
+	}, nil
+}
diff --git a/connector/openshift/openshift_test.go b/connector/openshift/openshift_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..474686e8ce5b5cfd88767e9b4184a863b4367f7a
--- /dev/null
+++ b/connector/openshift/openshift_test.go
@@ -0,0 +1,223 @@
+package openshift
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"reflect"
+	"testing"
+
+	"github.com/dexidp/dex/connector"
+	"github.com/dexidp/dex/storage/kubernetes/k8sapi"
+	"golang.org/x/oauth2"
+
+	"github.com/sirupsen/logrus"
+)
+
+func TestOpen(t *testing.T) {
+
+	s := newTestServer(map[string]interface{}{})
+	defer s.Close()
+
+	hostURL, err := url.Parse(s.URL)
+	expectNil(t, err)
+
+	_, err = http.NewRequest("GET", hostURL.String(), nil)
+	expectNil(t, err)
+
+	c := Config{
+		Issuer:       s.URL,
+		ClientID:     "testClientId",
+		ClientSecret: "testClientSecret",
+		RedirectURI:  "https://localhost/callback",
+		InsecureCA:   true,
+	}
+
+	logger := logrus.New()
+
+	oconfig, err := c.Open("id", logger)
+
+	oc, ok := oconfig.(*openshiftConnector)
+
+	expectNil(t, err)
+	expectEquals(t, ok, true)
+	expectEquals(t, oc.apiURL, s.URL)
+	expectEquals(t, oc.clientID, "testClientId")
+	expectEquals(t, oc.clientSecret, "testClientSecret")
+	expectEquals(t, oc.redirectURI, "https://localhost/callback")
+	expectEquals(t, oc.oauth2Config.Endpoint.AuthURL, fmt.Sprintf("%s/oauth/authorize", s.URL))
+	expectEquals(t, oc.oauth2Config.Endpoint.TokenURL, fmt.Sprintf("%s/oauth/token", s.URL))
+}
+
+func TestGetUser(t *testing.T) {
+
+	s := newTestServer(map[string]interface{}{
+		"/apis/user.openshift.io/v1/users/~": user{
+			ObjectMeta: k8sapi.ObjectMeta{
+				Name: "jdoe",
+			},
+			FullName: "John Doe",
+			Groups:   []string{"users"},
+		},
+	})
+	defer s.Close()
+
+	hostURL, err := url.Parse(s.URL)
+	expectNil(t, err)
+
+	_, err = http.NewRequest("GET", hostURL.String(), nil)
+	expectNil(t, err)
+
+	h, err := newHTTPClient(true, "")
+
+	expectNil(t, err)
+
+	oc := openshiftConnector{apiURL: s.URL, httpClient: h}
+	u, err := oc.user(context.Background(), h)
+
+	expectNil(t, err)
+	expectEquals(t, u.Name, "jdoe")
+	expectEquals(t, u.FullName, "John Doe")
+	expectEquals(t, len(u.Groups), 1)
+
+}
+
+func TestVerifyGroupFn(t *testing.T) {
+
+	requiredGroups := []string{"users"}
+	groupMembership := []string{"users","org1"}
+
+	validGroupMembership := validateRequiredGroups(groupMembership, requiredGroups)
+
+	expectEquals(t, validGroupMembership, true)
+
+}
+
+func TestVerifyGroup(t *testing.T) {
+
+	s := newTestServer(map[string]interface{}{
+		"/apis/user.openshift.io/v1/users/~": user{
+			ObjectMeta: k8sapi.ObjectMeta{
+				Name: "jdoe",
+			},
+			FullName: "John Doe",
+			Groups:   []string{"users"},
+		},
+	})
+	defer s.Close()
+
+	hostURL, err := url.Parse(s.URL)
+	expectNil(t, err)
+
+	_, err = http.NewRequest("GET", hostURL.String(), nil)
+	expectNil(t, err)
+
+	h, err := newHTTPClient(true, "")
+
+	expectNil(t, err)
+
+	oc := openshiftConnector{apiURL: s.URL, httpClient: h}
+	u, err := oc.user(context.Background(), h)
+
+	expectNil(t, err)
+	expectEquals(t, u.Name, "jdoe")
+	expectEquals(t, u.FullName, "John Doe")
+	expectEquals(t, len(u.Groups), 1)
+
+}
+
+func TestCallbackIdentity(t *testing.T) {
+
+	s := newTestServer(map[string]interface{}{
+		"/apis/user.openshift.io/v1/users/~": user{
+			ObjectMeta: k8sapi.ObjectMeta{
+				Name: "jdoe",
+				UID:  "12345",
+			},
+			FullName: "John Doe",
+			Groups:   []string{"users"},
+		},
+		"/oauth/token": map[string]interface{}{
+			"access_token": "oRzxVjCnohYRHEYEhZshkmakKmoyVoTjfUGC",
+			"expires_in":   "30",
+		},
+	})
+	defer s.Close()
+
+	hostURL, err := url.Parse(s.URL)
+	expectNil(t, err)
+
+	req, err := http.NewRequest("GET", hostURL.String(), nil)
+	expectNil(t, err)
+
+	h, err := newHTTPClient(true, "")
+
+	expectNil(t, err)
+
+	oc := openshiftConnector{apiURL: s.URL, httpClient: h, oauth2Config: &oauth2.Config{
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  fmt.Sprintf("%s/oauth/authorize", s.URL),
+			TokenURL: fmt.Sprintf("%s/oauth/token", s.URL),
+		},
+	}}
+	identity, err := oc.HandleCallback(connector.Scopes{Groups: true}, req)
+
+	expectNil(t, err)
+	expectEquals(t, identity.UserID, "12345")
+	expectEquals(t, identity.Username, "jdoe")
+	expectEquals(t, identity.PreferredUsername, "jdoe")
+	expectEquals(t, len(identity.Groups), 1)
+	expectEquals(t, identity.Groups[0], "users")
+}
+
+func newTestServer(responses map[string]interface{}) *httptest.Server {
+
+	var s *httptest.Server
+	s = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+		responses["/.well-known/oauth-authorization-server"] = map[string]interface{}{
+			"issuer":                           fmt.Sprintf("%s", s.URL),
+			"authorization_endpoint":           fmt.Sprintf("%s/oauth/authorize", s.URL),
+			"token_endpoint":                   fmt.Sprintf("%s/oauth/token", s.URL),
+			"scopes_supported":                 []string{"user:full", "user:info", "user:check-access", "user:list-scoped-projects", "user:list-projects"},
+			"response_types_supported":         []string{"token", "code"},
+			"grant_types_supported":            []string{"authorization_code", "implicit"},
+			"code_challenge_methods_supported": []string{"plain", "S256"},
+		}
+
+		response := responses[r.RequestURI]
+		w.Header().Add("Content-Type", "application/json")
+		json.NewEncoder(w).Encode(response)
+	}))
+
+	return s
+}
+
+func newClient() *http.Client {
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+	}
+	return &http.Client{Transport: tr}
+}
+
+func expectNil(t *testing.T, a interface{}) {
+	if a != nil {
+		t.Errorf("Expected %+v to equal nil", a)
+	}
+}
+
+func expectNotNil(t *testing.T, a interface{}, msg string) {
+	if a == nil {
+		t.Errorf("Expected %+v to not to be nil", msg)
+	}
+}
+
+func expectEquals(t *testing.T, a interface{}, b interface{}) {
+	if !reflect.DeepEqual(a, b) {
+		t.Errorf("Expected %+v to equal %+v", a, b)
+	}
+}
diff --git a/server/server.go b/server/server.go
index 4dc7337d033e0765d9586647d4e56aff40d9995d..27d93064676f349119f7c19150a42033d9cfdc5f 100644
--- a/server/server.go
+++ b/server/server.go
@@ -32,6 +32,7 @@ import (
 	"github.com/dexidp/dex/connector/microsoft"
 	"github.com/dexidp/dex/connector/mock"
 	"github.com/dexidp/dex/connector/oidc"
+	"github.com/dexidp/dex/connector/openshift"
 	"github.com/dexidp/dex/connector/saml"
 	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
@@ -461,6 +462,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{
 	"linkedin":        func() ConnectorConfig { return new(linkedin.Config) },
 	"microsoft":       func() ConnectorConfig { return new(microsoft.Config) },
 	"bitbucket-cloud": func() ConnectorConfig { return new(bitbucketcloud.Config) },
+	"openshift":       func() ConnectorConfig { return new(openshift.Config) },
 	// Keep around for backwards compatibility.
 	"samlExperimental": func() ConnectorConfig { return new(saml.Config) },
 }