diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41e5a70835a6a4e4aa6d481a47365d1046487712..7f6301e9ae150dbd38f2648f7f2bff270b7bb258 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@
 
 ## Changes since v5.1.0
 
+- [#499](https://github.com/oauth2-proxy/oauth2-proxy/pull/469) Add `-user-id-claim` to support generic claims in addition to email
 - [#486](https://github.com/oauth2-proxy/oauth2-proxy/pull/486) Add new linters (@johejo)
 - [#440](https://github.com/oauth2-proxy/oauth2-proxy/pull/440) Switch Azure AD Graph API to Microsoft Graph API (@johejo)
 - [#453](https://github.com/oauth2-proxy/oauth2-proxy/pull/453) Prevent browser caching during auth flow (@johejo)
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index e528cd8d2a71a74770964a41dab61cd4f29da854..6c6d10da95aa90dc874496a476b5017223f13a7f 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -119,6 +119,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example
 | `-tls-cert-file` | string | path to certificate file | |
 | `-tls-key-file` | string | path to private key file | |
 | `-upstream` | string \| list | the http url(s) of the upstream endpoint, file:// paths for static files or `static://<status_code>` for static response. Routing is based on the path | |
+| `-user-id-claim` | string | which claim contains the user ID | \["email"\] |
 | `-validate-url` | string | Access token validation endpoint | |
 | `-version` | n/a | print version string | |
 | `-whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (eg `.example.com`) | |
diff --git a/main.go b/main.go
index 33a6c87d3ba16191f482a2b20ed3424f57df60f3..91111eaa691a1d796259818ae868057aa417b329 100644
--- a/main.go
+++ b/main.go
@@ -147,6 +147,8 @@ func main() {
 	flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov")
 	flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints")
 
+	flagSet.String("user-id-claim", "email", "which claim contains the user ID")
+
 	flagSet.Parse(os.Args[1:])
 
 	if *showVersion {
diff --git a/oauthproxy.go b/oauthproxy.go
index d788065db33ac05c5efb60c607770c36d1b5191c..1e9bb7cafe873fa438a79e80b78f7289cfdfcb7c 100644
--- a/oauthproxy.go
+++ b/oauthproxy.go
@@ -1115,6 +1115,7 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
 }
 
 // GetJwtSession loads a session based on a JWT token in the authorization header.
+// (see the config options skip-jwt-bearer-tokens and extra-jwt-issuers)
 func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
 	rawBearerToken, err := p.findBearerToken(req)
 	if err != nil {
@@ -1122,7 +1123,6 @@ func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState
 	}
 
 	ctx := context.Background()
-	var session *sessionsapi.SessionState
 	for _, verifier := range p.jwtBearerVerifiers {
 		bearerToken, err := verifier.Verify(ctx, rawBearerToken)
 
@@ -1131,35 +1131,7 @@ func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState
 			continue
 		}
 
-		var claims struct {
-			Subject           string `json:"sub"`
-			Email             string `json:"email"`
-			Verified          *bool  `json:"email_verified"`
-			PreferredUsername string `json:"preferred_username"`
-		}
-
-		if err := bearerToken.Claims(&claims); err != nil {
-			return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
-		}
-
-		if claims.Email == "" {
-			claims.Email = claims.Subject
-		}
-
-		if claims.Verified != nil && !*claims.Verified {
-			return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
-		}
-
-		session = &sessionsapi.SessionState{
-			AccessToken:       rawBearerToken,
-			IDToken:           rawBearerToken,
-			RefreshToken:      "",
-			ExpiresOn:         bearerToken.Expiry,
-			Email:             claims.Email,
-			User:              claims.Email,
-			PreferredUsername: claims.PreferredUsername,
-		}
-		return session, nil
+		return p.provider.CreateSessionStateFromBearerToken(rawBearerToken, bearerToken)
 	}
 	return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
 }
diff --git a/options.go b/options.go
index 1b6b962aefc2f099948fe0cac174c6c72a9b7fea..50dee28b85495b8fddf0aacfdce04d750f223e8d 100644
--- a/options.go
+++ b/options.go
@@ -107,6 +107,7 @@ type Options struct {
 	Scope                              string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"`
 	Prompt                             string `flag:"prompt" cfg:"prompt" env:"OAUTH2_PROXY_PROMPT"`
 	ApprovalPrompt                     string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"` // Deprecated by OIDC 1.0
+	UserIDClaim                        string `flag:"user-id-claim" cfg:"user_id_claim" env:"OAUTH2_PROXY_USER_ID_CLAIM"`
 
 	// Configuration values for logging
 	LoggingFilename       string `flag:"logging-filename" cfg:"logging_filename" env:"OAUTH2_PROXY_LOGGING_FILENAME"`
@@ -179,6 +180,7 @@ func NewOptions() *Options {
 		PreferEmailToUser:                false,
 		Prompt:                           "", // Change to "login" when ApprovalPrompt officially deprecated
 		ApprovalPrompt:                   "force",
+		UserIDClaim:                      "email",
 		InsecureOIDCAllowUnverifiedEmail: false,
 		SkipOIDCDiscovery:                false,
 		LoggingFilename:                  "",
@@ -500,6 +502,7 @@ func parseProviderInfo(o *Options, msgs []string) []string {
 		p.SetRepository(o.BitbucketRepository)
 	case *providers.OIDCProvider:
 		p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
+		p.UserIDClaim = o.UserIDClaim
 		if o.oidcVerifier == nil {
 			msgs = append(msgs, "oidc provider requires an oidc issuer URL")
 		} else {
diff --git a/providers/oidc.go b/providers/oidc.go
index ed982e1a9dcddd6d7cbd2e7aaabac37a066fb9a7..ac27c8aa375a8515611e4a15399f3dda6dcf0f2a 100644
--- a/providers/oidc.go
+++ b/providers/oidc.go
@@ -14,12 +14,15 @@ import (
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
 )
 
+const emailClaim = "email"
+
 // OIDCProvider represents an OIDC based Identity Provider
 type OIDCProvider struct {
 	*ProviderData
 
 	Verifier             *oidc.IDTokenVerifier
 	AllowUnverifiedEmail bool
+	UserIDClaim          string
 }
 
 // NewOIDCProvider initiates a new OIDCProvider
@@ -148,24 +151,15 @@ func (p *OIDCProvider) findVerifiedIDToken(ctx context.Context, token *oauth2.To
 
 func (p *OIDCProvider) createSessionState(token *oauth2.Token, idToken *oidc.IDToken) (*sessions.SessionState, error) {
 
-	newSession := &sessions.SessionState{}
+	var newSession *sessions.SessionState
 
-	if idToken != nil {
-		claims, err := findClaimsFromIDToken(idToken, token.AccessToken, p.ProfileURL.String())
+	if idToken == nil {
+		newSession = &sessions.SessionState{}
+	} else {
+		var err error
+		newSession, err = p.createSessionStateInternal(token.Extra("id_token").(string), idToken, token)
 		if err != nil {
-			return nil, fmt.Errorf("couldn't extract claims from id_token (%e)", err)
-		}
-
-		if claims != nil {
-
-			if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified {
-				return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
-			}
-
-			newSession.IDToken = token.Extra("id_token").(string)
-			newSession.Email = claims.Email
-			newSession.User = claims.Subject
-			newSession.PreferredUsername = claims.PreferredUsername
+			return nil, err
 		}
 	}
 
@@ -176,6 +170,52 @@ func (p *OIDCProvider) createSessionState(token *oauth2.Token, idToken *oidc.IDT
 	return newSession, nil
 }
 
+func (p *OIDCProvider) CreateSessionStateFromBearerToken(rawIDToken string, idToken *oidc.IDToken) (*sessions.SessionState, error) {
+	newSession, err := p.createSessionStateInternal(rawIDToken, idToken, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	newSession.AccessToken = rawIDToken
+	newSession.IDToken = rawIDToken
+	newSession.RefreshToken = ""
+	newSession.ExpiresOn = idToken.Expiry
+
+	return newSession, nil
+}
+
+func (p *OIDCProvider) createSessionStateInternal(rawIDToken string, idToken *oidc.IDToken, token *oauth2.Token) (*sessions.SessionState, error) {
+
+	newSession := &sessions.SessionState{}
+
+	if idToken == nil {
+		return newSession, nil
+	}
+	accessToken := ""
+	if token != nil {
+		accessToken = token.AccessToken
+	}
+
+	claims, err := p.findClaimsFromIDToken(idToken, accessToken, p.ProfileURL.String())
+	if err != nil {
+		return nil, fmt.Errorf("couldn't extract claims from id_token (%e)", err)
+	}
+
+	newSession.IDToken = rawIDToken
+
+	newSession.Email = claims.UserID // TODO Rename SessionState.Email to .UserID in the near future
+
+	newSession.User = claims.Subject
+	newSession.PreferredUsername = claims.PreferredUsername
+
+	verifyEmail := (p.UserIDClaim == emailClaim) && !p.AllowUnverifiedEmail
+	if verifyEmail && claims.Verified != nil && !*claims.Verified {
+		return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.UserID)
+	}
+
+	return newSession, nil
+}
+
 // ValidateSessionState checks that the session's IDToken is still valid
 func (p *OIDCProvider) ValidateSessionState(s *sessions.SessionState) bool {
 	ctx := context.Background()
@@ -190,15 +230,25 @@ func getOIDCHeader(accessToken string) http.Header {
 	return header
 }
 
-func findClaimsFromIDToken(idToken *oidc.IDToken, accessToken string, profileURL string) (*OIDCClaims, error) {
+func (p *OIDCProvider) findClaimsFromIDToken(idToken *oidc.IDToken, accessToken string, profileURL string) (*OIDCClaims, error) {
 
-	// Extract custom claims.
 	claims := &OIDCClaims{}
-	if err := idToken.Claims(claims); err != nil {
-		return nil, fmt.Errorf("failed to parse id_token claims: %v", err)
+	// Extract default claims.
+	if err := idToken.Claims(&claims); err != nil {
+		return nil, fmt.Errorf("failed to parse default id_token claims: %v", err)
+	}
+	// Extract custom claims.
+	if err := idToken.Claims(&claims.rawClaims); err != nil {
+		return nil, fmt.Errorf("failed to parse all id_token claims: %v", err)
+	}
+
+	userID := claims.rawClaims[p.UserIDClaim]
+	if userID == nil {
+		return nil, fmt.Errorf("claims did not contains the required user-id-claim '%s'", p.UserIDClaim)
 	}
+	claims.UserID = fmt.Sprint(userID)
 
-	if claims.Email == "" {
+	if p.UserIDClaim == emailClaim && claims.UserID == "" {
 		if profileURL == "" {
 			return nil, fmt.Errorf("id_token did not contain an email")
 		}
@@ -223,15 +273,16 @@ func findClaimsFromIDToken(idToken *oidc.IDToken, accessToken string, profileURL
 			return nil, fmt.Errorf("neither id_token nor userinfo endpoint contained an email")
 		}
 
-		claims.Email = email
+		claims.UserID = email
 	}
 
 	return claims, nil
 }
 
 type OIDCClaims struct {
+	rawClaims         map[string]interface{}
+	UserID            string
 	Subject           string `json:"sub"`
-	Email             string `json:"email"`
 	Verified          *bool  `json:"email_verified"`
 	PreferredUsername string `json:"preferred_username"`
 }
diff --git a/providers/oidc_test.go b/providers/oidc_test.go
index 389568bff5075fa0d745f11fe9f2c5e1f78a6343..b0596e36036df79920e34dff3dd01b8e73270a6a 100644
--- a/providers/oidc_test.go
+++ b/providers/oidc_test.go
@@ -31,6 +31,7 @@ const secret = "secret"
 type idTokenClaims struct {
 	Name    string `json:"name,omitempty"`
 	Email   string `json:"email,omitempty"`
+	Phone   string `json:"phone_number,omitempty"`
 	Picture string `json:"picture,omitempty"`
 	jwt.StandardClaims
 }
@@ -46,6 +47,7 @@ type redeemTokenResponse struct {
 var defaultIDToken idTokenClaims = idTokenClaims{
 	"Jane Dobbs",
 	"janed@me.com",
+	"+4798765432",
 	"http://mugbook.com/janed/me.jpg",
 	jwt.StandardClaims{
 		Audience:  "https://test.myapp.com",
@@ -106,6 +108,7 @@ func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
 			fakeKeySetStub{},
 			&oidc.Config{ClientID: clientID},
 		),
+		UserIDClaim: "email",
 	}
 
 	return p
@@ -165,6 +168,26 @@ func TestOIDCProviderRedeem(t *testing.T) {
 	assert.Equal(t, "123456789", session.User)
 }
 
+func TestOIDCProviderRedeem_custom_userid(t *testing.T) {
+
+	idToken, _ := newSignedTestIDToken(defaultIDToken)
+	body, _ := json.Marshal(redeemTokenResponse{
+		AccessToken:  accessToken,
+		ExpiresIn:    10,
+		TokenType:    "Bearer",
+		RefreshToken: refreshToken,
+		IDToken:      idToken,
+	})
+
+	server, provider := newTestSetup(body)
+	provider.UserIDClaim = "phone_number"
+	defer server.Close()
+
+	session, err := provider.Redeem(provider.RedeemURL.String(), "code1234")
+	assert.Equal(t, nil, err)
+	assert.Equal(t, defaultIDToken.Phone, session.Email)
+}
+
 func TestOIDCProviderRefreshSessionIfNeededWithoutIdToken(t *testing.T) {
 
 	idToken, _ := newSignedTestIDToken(defaultIDToken)
diff --git a/providers/provider_default.go b/providers/provider_default.go
index 1daf6d53628a376ee811397458415cb22826f4ef..720dd580761e1a7e9290bc30ee6e8a0ac8bedb7d 100644
--- a/providers/provider_default.go
+++ b/providers/provider_default.go
@@ -10,6 +10,8 @@ import (
 	"net/url"
 	"time"
 
+	"github.com/coreos/go-oidc"
+
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
 )
@@ -144,3 +146,37 @@ func (p *ProviderData) ValidateSessionState(s *sessions.SessionState) bool {
 func (p *ProviderData) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
 	return false, nil
 }
+
+func (p *ProviderData) CreateSessionStateFromBearerToken(rawIDToken string, idToken *oidc.IDToken) (*sessions.SessionState, error) {
+	var claims struct {
+		Subject           string `json:"sub"`
+		Email             string `json:"email"`
+		Verified          *bool  `json:"email_verified"`
+		PreferredUsername string `json:"preferred_username"`
+	}
+
+	if err := idToken.Claims(&claims); err != nil {
+		return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
+	}
+
+	if claims.Email == "" {
+		claims.Email = claims.Subject
+	}
+
+	if claims.Verified != nil && !*claims.Verified {
+		return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
+	}
+
+	newSession := &sessions.SessionState{
+		Email:             claims.Email,
+		User:              claims.Email,
+		PreferredUsername: claims.PreferredUsername,
+	}
+
+	newSession.AccessToken = rawIDToken
+	newSession.IDToken = rawIDToken
+	newSession.RefreshToken = ""
+	newSession.ExpiresOn = idToken.Expiry
+
+	return newSession, nil
+}
diff --git a/providers/providers.go b/providers/providers.go
index 97cc17a7ec1521bc0eccfeff8831a3bd00e660e7..20c4248942d70d22393561bc9e14ad23fa58efac 100644
--- a/providers/providers.go
+++ b/providers/providers.go
@@ -1,6 +1,7 @@
 package providers
 
 import (
+	"github.com/coreos/go-oidc"
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
 )
@@ -18,6 +19,7 @@ type Provider interface {
 	RefreshSessionIfNeeded(*sessions.SessionState) (bool, error)
 	SessionFromCookie(string, *encryption.Cipher) (*sessions.SessionState, error)
 	CookieForSession(*sessions.SessionState, *encryption.Cipher) (string, error)
+	CreateSessionStateFromBearerToken(rawIDToken string, idToken *oidc.IDToken) (*sessions.SessionState, error)
 }
 
 // New provides a new Provider based on the configured provider string