diff --git a/examples/k8s/README.md b/examples/k8s/README.md
index 5162590d7d57ff44f8bddd2394c5be61b8ce374b..1b195199d8d1ff2afe978869de06c4bb30eef62f 100644
--- a/examples/k8s/README.md
+++ b/examples/k8s/README.md
@@ -28,7 +28,6 @@ ConfigMap for dex to use. These run dex as a deployment with configuration and
 storage, allowing it to get started. 
 
 ```
-kubectl create -f thirdpartyresources.yaml
 kubectl create configmap dex-config --from-file=config.yaml=config-k8s.yaml
 kubectl create -f deployment.yaml
 ```
diff --git a/examples/k8s/thirdpartyresources.yaml b/examples/k8s/thirdpartyresources.yaml
deleted file mode 100644
index 40f03027259236120fa1b854c4b5d9b24d7fbe31..0000000000000000000000000000000000000000
--- a/examples/k8s/thirdpartyresources.yaml
+++ /dev/null
@@ -1,57 +0,0 @@
-# NOTE: Because of a bug in third party resources, each resource must be in it's
-# own API Group.
-#
-# See fix at https://github.com/kubernetes/kubernetes/pull/28414
-
-metadata:
-  name: auth-code.authcodes.oidc.coreos.com
-apiVersion: extensions/v1beta1
-kind: ThirdPartyResource
-description: "A code which can be claimed for an access token."
-versions:
-- name: v1
----
-
-metadata:
-  name: auth-request.authrequests.oidc.coreos.com
-apiVersion: extensions/v1beta1
-kind: ThirdPartyResource
-description: "A request for an end user to authorize a client."
-versions:
-- name: v1
----
-
-metadata:
-  name: o-auth2-client.oauth2clients.oidc.coreos.com
-apiVersion: extensions/v1beta1
-kind: ThirdPartyResource
-description: "An OpenID Connect client."
-versions:
-- name: v1
----
-
-metadata:
-  name: signing-key.signingkeies.oidc.coreos.com
-apiVersion: extensions/v1beta1
-kind: ThirdPartyResource
-description: "Keys used to sign and verify OpenID Connect tokens."
-versions:
-- name: v1
----
-
-metadata:
-  name: refresh-token.refreshtokens.oidc.coreos.com
-apiVersion: extensions/v1beta1
-kind: ThirdPartyResource
-description: "Refresh tokens for clients to continuously act on behalf of an end user."
-versions:
-- name: v1
----
-
-metadata:
-  name: password.passwords.oidc.coreos.com
-apiVersion: extensions/v1beta1
-kind: ThirdPartyResource
-description: "Passwords managed by the OIDC server."
-versions:
-- name: v1
diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go
index 2c0910a7334c47d88a8a06463ff92a16c4480cb3..bfdc915113c9315d248d34dc66de961e8221a807 100644
--- a/storage/kubernetes/client.go
+++ b/storage/kubernetes/client.go
@@ -20,6 +20,7 @@ import (
 	"time"
 
 	"github.com/gtank/cryptopasta"
+	"golang.org/x/net/context"
 	yaml "gopkg.in/yaml.v2"
 
 	"github.com/coreos/dex/storage"
@@ -27,27 +28,17 @@ import (
 )
 
 type client struct {
-	client     *http.Client
-	baseURL    string
-	namespace  string
-	apiVersion string
+	client    *http.Client
+	baseURL   string
+	namespace string
 
-	now func() time.Time
+	// API version of the oidc resources. For example "oidc.coreos.com". This is
+	// currently not configurable, but could be in the future.
+	apiVersion string
 
-	// BUG: currently each third party API group can only have one resource in it,
-	// so for each resource this storage uses, it need a unique API group.
-	//
-	// Prepend the name of each resource to the API group for a predictable mapping.
-	//
-	// See: https://github.com/kubernetes/kubernetes/pull/28414
-	prependResourceNameToAPIGroup bool
-}
-
-func (c *client) apiVersionForResource(resource string) string {
-	if !c.prependResourceNameToAPIGroup {
-		return c.apiVersion
-	}
-	return resource + "." + c.apiVersion
+	// This is called once the client's Close method is called to signal goroutines,
+	// such as the one creating third party resources, to stop.
+	cancel context.CancelFunc
 }
 
 func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
@@ -56,10 +47,6 @@ func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
 		basePath = "api/"
 	}
 
-	if c.prependResourceNameToAPIGroup && apiVersion != "" && resource != "" {
-		apiVersion = resource + "." + apiVersion
-	}
-
 	var p string
 	if namespace != "" {
 		p = path.Join(basePath, apiVersion, "namespaces", namespace, resource, name)
@@ -72,15 +59,28 @@ func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
 	return c.baseURL + "/" + p
 }
 
+// Define an error interface so we can get at the underlying status code if it's
+// absolutely necessary. For instance when we need to see if an error indicates
+// a resource already exists.
+type httpError interface {
+	StatusCode() int
+}
+
+var _ httpError = (*httpErr)(nil)
+
 type httpErr struct {
 	method string
 	url    string
-	status string
+	status int
 	body   []byte
 }
 
+func (e *httpErr) StatusCode() int {
+	return e.status
+}
+
 func (e *httpErr) Error() string {
-	return fmt.Sprintf("%s %s %s: response from server \"%s\"", e.method, e.url, e.status, bytes.TrimSpace(e.body))
+	return fmt.Sprintf("%s %s %s: response from server \"%s\"", e.method, e.url, http.StatusText(e.status), bytes.TrimSpace(e.body))
 }
 
 func checkHTTPErr(r *http.Response, validStatusCodes ...int) error {
@@ -100,7 +100,7 @@ func checkHTTPErr(r *http.Response, validStatusCodes ...int) error {
 		method = r.Request.Method
 		url = r.Request.URL.String()
 	}
-	err = &httpErr{method, url, r.Status, body}
+	err = &httpErr{method, url, r.StatusCode, body}
 	log.Printf("%s", err)
 
 	if r.StatusCode == http.StatusNotFound {
@@ -134,12 +134,16 @@ func (c *client) list(resource string, v interface{}) error {
 }
 
 func (c *client) post(resource string, v interface{}) error {
+	return c.postResource(c.apiVersion, c.namespace, resource, v)
+}
+
+func (c *client) postResource(apiVersion, namespace, resource string, v interface{}) error {
 	body, err := json.Marshal(v)
 	if err != nil {
 		return fmt.Errorf("marshal object: %v", err)
 	}
 
-	url := c.urlFor(c.apiVersion, c.namespace, resource, "")
+	url := c.urlFor(apiVersion, namespace, resource, "")
 	resp, err := c.client.Post(url, "application/json", bytes.NewReader(body))
 	if err != nil {
 		return err
@@ -277,8 +281,6 @@ func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string) (
 		baseURL:    cluster.Server,
 		namespace:  namespace,
 		apiVersion: "oidc.coreos.com/v1",
-		now:        time.Now,
-		prependResourceNameToAPIGroup: true,
 	}, nil
 }
 
diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go
index 178a90db5c781978d6e61e08dbd1ccf2f98974e2..7a7fb2b465233d502a26894d3b3171ba71aef932 100644
--- a/storage/kubernetes/storage.go
+++ b/storage/kubernetes/storage.go
@@ -4,11 +4,13 @@ import (
 	"errors"
 	"fmt"
 	"log"
+	"net/http"
 	"os"
 	"path/filepath"
 	"time"
 
 	homedir "github.com/mitchellh/go-homedir"
+	"golang.org/x/net/context"
 
 	"github.com/coreos/dex/storage"
 	"github.com/coreos/dex/storage/kubernetes/k8sapi"
@@ -45,7 +47,6 @@ func (c *Config) Open() (storage.Storage, error) {
 	if err != nil {
 		return nil, err
 	}
-
 	return cli, nil
 }
 
@@ -81,10 +82,57 @@ func (c *Config) open() (*client, error) {
 		return nil, err
 	}
 
-	return newClient(cluster, user, namespace)
+	cli, err := newClient(cluster, user, namespace)
+	if err != nil {
+		return nil, fmt.Errorf("create client: %v", err)
+	}
+
+	// Don't try to synchronize this because creating third party resources is not
+	// a synchronous event. Even after the API server returns a 200, it can still
+	// take several seconds for them to actually appear.
+	ctx, cancel := context.WithCancel(context.Background())
+	go func() {
+		for {
+			if err := cli.createThirdPartyResources(); err != nil {
+				log.Printf("failed creating third party resources: %v", err)
+			} else {
+				return
+			}
+
+			select {
+			case <-ctx.Done():
+				return
+			case <-time.After(30 * time.Second):
+			}
+		}
+	}()
+
+	// If the client is closed, stop trying to create third party resources.
+	cli.cancel = cancel
+	return cli, nil
+}
+
+func (cli *client) createThirdPartyResources() error {
+	for _, r := range thirdPartyResources {
+		err := cli.postResource("extensions/v1beta1", "", "thirdpartyresources", r)
+		if err != nil {
+			if e, ok := err.(httpError); ok {
+				if e.StatusCode() == http.StatusConflict {
+					log.Printf("third party resource already created %q", r.ObjectMeta.Name)
+					continue
+				}
+			}
+			return err
+		}
+		log.Printf("create third party resource %q", r.ObjectMeta.Name)
+	}
+	return nil
 }
 
 func (cli *client) Close() error {
+	if cli.cancel != nil {
+		cli.cancel()
+	}
 	return nil
 }
 
@@ -108,7 +156,7 @@ func (cli *client) CreateRefresh(r storage.RefreshToken) error {
 	refresh := RefreshToken{
 		TypeMeta: k8sapi.TypeMeta{
 			Kind:       kindRefreshToken,
-			APIVersion: cli.apiVersionForResource(resourceRefreshToken),
+			APIVersion: cli.apiVersion,
 		},
 		ObjectMeta: k8sapi.ObjectMeta{
 			Name:      r.RefreshToken,
diff --git a/storage/kubernetes/storage_test.go b/storage/kubernetes/storage_test.go
index 043a1e9fb2a8a559ae9215c77d69d1b3869a68d0..0f347730c3c77ed0f86ddaad17f42fb7850dbbb2 100644
--- a/storage/kubernetes/storage_test.go
+++ b/storage/kubernetes/storage_test.go
@@ -60,7 +60,7 @@ func TestURLFor(t *testing.T) {
 	}
 
 	for _, test := range tests {
-		c := &client{baseURL: test.baseURL, prependResourceNameToAPIGroup: false}
+		c := &client{baseURL: test.baseURL}
 		got := c.urlFor(test.apiVersion, test.namespace, test.resource, test.name)
 		if got != test.want {
 			t.Errorf("(&client{baseURL:%q}).urlFor(%q, %q, %q, %q): expected %q got %q",
diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go
index 3c914e844a4ce1c6c8ade9c0aa6be743f0c6b8e1..8c8f9d6eee91072d4d9c99b83bdfdbbcd6ffe07a 100644
--- a/storage/kubernetes/types.go
+++ b/storage/kubernetes/types.go
@@ -11,6 +11,64 @@ import (
 	"github.com/coreos/dex/storage/kubernetes/k8sapi"
 )
 
+var tprMeta = k8sapi.TypeMeta{
+	APIVersion: "extensions/v1beta1",
+	Kind:       "ThirdPartyResource",
+}
+
+// The set of third party resources required by the storage. These are managed by
+// the storage so it can migrate itself by creating new resources.
+var thirdPartyResources = []k8sapi.ThirdPartyResource{
+	{
+		ObjectMeta: k8sapi.ObjectMeta{
+			Name: "auth-code.oidc.coreos.com",
+		},
+		TypeMeta:    tprMeta,
+		Description: "A code which can be claimed for an access token.",
+		Versions:    []k8sapi.APIVersion{{Name: "v1"}},
+	},
+	{
+		ObjectMeta: k8sapi.ObjectMeta{
+			Name: "auth-request.oidc.coreos.com",
+		},
+		TypeMeta:    tprMeta,
+		Description: "A request for an end user to authorize a client.",
+		Versions:    []k8sapi.APIVersion{{Name: "v1"}},
+	},
+	{
+		ObjectMeta: k8sapi.ObjectMeta{
+			Name: "o-auth2-client.oidc.coreos.com",
+		},
+		TypeMeta:    tprMeta,
+		Description: "An OpenID Connect client.",
+		Versions:    []k8sapi.APIVersion{{Name: "v1"}},
+	},
+	{
+		ObjectMeta: k8sapi.ObjectMeta{
+			Name: "signing-key.oidc.coreos.com",
+		},
+		TypeMeta:    tprMeta,
+		Description: "Keys used to sign and verify OpenID Connect tokens.",
+		Versions:    []k8sapi.APIVersion{{Name: "v1"}},
+	},
+	{
+		ObjectMeta: k8sapi.ObjectMeta{
+			Name: "refresh-token.oidc.coreos.com",
+		},
+		TypeMeta:    tprMeta,
+		Description: "Refresh tokens for clients to continuously act on behalf of an end user.",
+		Versions:    []k8sapi.APIVersion{{Name: "v1"}},
+	},
+	{
+		ObjectMeta: k8sapi.ObjectMeta{
+			Name: "password.oidc.coreos.com",
+		},
+		TypeMeta:    tprMeta,
+		Description: "Passwords managed by the OIDC server.",
+		Versions:    []k8sapi.APIVersion{{Name: "v1"}},
+	},
+}
+
 // There will only ever be a single keys resource. Maintain this by setting a
 // common name.
 const keysName = "openid-connect-keys"
@@ -45,7 +103,7 @@ func (cli *client) fromStorageClient(c storage.Client) Client {
 	return Client{
 		TypeMeta: k8sapi.TypeMeta{
 			Kind:       kindClient,
-			APIVersion: cli.apiVersionForResource(resourceClient),
+			APIVersion: cli.apiVersion,
 		},
 		ObjectMeta: k8sapi.ObjectMeta{
 			Name:      c.ID,
@@ -162,7 +220,7 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
 	req := AuthRequest{
 		TypeMeta: k8sapi.TypeMeta{
 			Kind:       kindAuthRequest,
-			APIVersion: cli.apiVersionForResource(resourceAuthRequest),
+			APIVersion: cli.apiVersion,
 		},
 		ObjectMeta: k8sapi.ObjectMeta{
 			Name:      a.ID,
@@ -216,7 +274,7 @@ func (cli *client) fromStoragePassword(p storage.Password) Password {
 	return Password{
 		TypeMeta: k8sapi.TypeMeta{
 			Kind:       kindPassword,
-			APIVersion: cli.apiVersionForResource(resourcePassword),
+			APIVersion: cli.apiVersion,
 		},
 		ObjectMeta: k8sapi.ObjectMeta{
 			Name:      emailToID(email),
@@ -270,7 +328,7 @@ func (cli *client) fromStorageAuthCode(a storage.AuthCode) AuthCode {
 	return AuthCode{
 		TypeMeta: k8sapi.TypeMeta{
 			Kind:       kindAuthCode,
-			APIVersion: cli.apiVersionForResource(resourceAuthCode),
+			APIVersion: cli.apiVersion,
 		},
 		ObjectMeta: k8sapi.ObjectMeta{
 			Name:      a.ID,
@@ -346,7 +404,7 @@ func (cli *client) fromStorageKeys(keys storage.Keys) Keys {
 	return Keys{
 		TypeMeta: k8sapi.TypeMeta{
 			Kind:       kindKeys,
-			APIVersion: cli.apiVersionForResource(resourceKeys),
+			APIVersion: cli.apiVersion,
 		},
 		ObjectMeta: k8sapi.ObjectMeta{
 			Name:      keysName,