diff --git a/Documentation/connectors/oidc.md b/Documentation/connectors/oidc.md
index c472e303ffe4f1d397da25be07ef1f0083755e3a..e64cd2de2ff963abae54240c4a9d372104aa0a6d 100644
--- a/Documentation/connectors/oidc.md
+++ b/Documentation/connectors/oidc.md
@@ -8,8 +8,6 @@ Prominent examples of OpenID Connect providers include Google Accounts, Salesfor
 
 ## Caveats
 
-This connector does not support the "groups" claim. Progress for this is tracked in [issue #1065][issue-1065].
-
 When using refresh tokens, changes to the upstream claims aren't propagated to the id_token returned by dex. If a user's email changes, the "email" claim returned by dex won't change unless the user logs in again. Progress for this is tracked in [issue #863][issue-863].
 
 ## Configuration
@@ -75,11 +73,10 @@ connectors:
     # getUserInfo: true
 
     # The set claim is used as user id.
-    # Default: sub
     # Claims list at https://openid.net/specs/openid-connect-core-1_0.html#Claims
-    #
+    # Default: sub
     # userIDKey: nickname
-    
+
     # The set claim is used as user name.
     # Default: name
     # userNameKey: nickname
@@ -88,9 +85,25 @@ connectors:
     # However this is not supported by all OIDC providers, some of them support different
     # value for prompt, like "prompt=login" or "prompt=none"
     # promptType: consent
+
+    # Some providers return non-standard claims (eg. mail).
+    # Use claimMapping to map those claims to standard claims:
+    # https://openid.net/specs/openid-connect-core-1_0.html#Claims
+    # claimMapping can only map a non-standard claim to a standard one if it's not returned in the id_token.
+    claimMapping:
+      # The set claim is used as preferred username.
+      # Default: preferred_username
+      # preferred_username: other_user_name
+
+      # The set claim is used as email.
+      # Default: email
+      # email: mail
+
+      # The set claim is used as groups.
+      # Default: groups
+      # groups: "cognito:groups"
 ```
 
 [oidc-doc]: openid-connect.md
 [issue-863]: https://github.com/dexidp/dex/issues/863
-[issue-1065]: https://github.com/dexidp/dex/issues/1065
 [azure-ad-v1]: https://github.com/coreos/go-oidc/issues/133
diff --git a/README.md b/README.md
index 412aa84e6950e6fc4d4dfd4216166f0b647f4018..432c2fa18b0fa9c7c9d2d0e7f6e832892879c611 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ Dex implements the following connectors:
 | [GitHub](Documentation/connectors/github.md) | yes | yes | yes | stable | |
 | [SAML 2.0](Documentation/connectors/saml.md) | no | yes | no | stable |
 | [GitLab](Documentation/connectors/gitlab.md) | yes | yes | yes | beta | |
-| [OpenID Connect](Documentation/connectors/oidc.md) | yes | no ([#1065][issue-1065]) | no | beta | Includes Salesforce, Azure, etc. |
+| [OpenID Connect](Documentation/connectors/oidc.md) | yes | yes | yes | beta | Includes Salesforce, Azure, etc. |
 | [Google](Documentation/connectors/google.md) | yes | yes | yes | alpha | |
 | [LinkedIn](Documentation/connectors/linkedin.md) | yes | no | no | beta | |
 | [Microsoft](Documentation/connectors/microsoft.md) | yes | yes | no | beta | |
diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go
index 675e4b95df5211cbc2968b6a07f1f054a1294048..b8e543d4bc6cb11c5b59b33853ca53f54c0baa77 100644
--- a/connector/oidc/oidc.go
+++ b/connector/oidc/oidc.go
@@ -49,14 +49,23 @@ type Config struct {
 	// id tokens
 	GetUserInfo bool `json:"getUserInfo"`
 
-	// Configurable key which contains the user id claim
 	UserIDKey string `json:"userIDKey"`
 
-	// Configurable key which contains the user name claim
 	UserNameKey string `json:"userNameKey"`
 
 	// PromptType will be used fot the prompt parameter (when offline_access, by default prompt=consent)
 	PromptType string `json:"promptType"`
+
+	ClaimMapping struct {
+		// Configurable key which contains the preferred username claims
+		PreferredUsernameKey string `json:"preferred_username"` // defaults to "preferred_username"
+
+		// Configurable key which contains the email claims
+		EmailKey string `json:"email"` // defaults to "email"
+
+		// Configurable key which contains the groups claims
+		GroupsKey string `json:"groups"` // defaults to "groups"
+	} `json:"claimMapping"`
 }
 
 // Domains that don't support basic auth. golang.org/x/oauth2 has an internal
@@ -141,9 +150,12 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
 		insecureSkipEmailVerified: c.InsecureSkipEmailVerified,
 		insecureEnableGroups:      c.InsecureEnableGroups,
 		getUserInfo:               c.GetUserInfo,
+		promptType:                c.PromptType,
 		userIDKey:                 c.UserIDKey,
 		userNameKey:               c.UserNameKey,
-		promptType:                c.PromptType,
+		preferredUsernameKey:      c.ClaimMapping.PreferredUsernameKey,
+		emailKey:                  c.ClaimMapping.EmailKey,
+		groupsKey:                 c.ClaimMapping.GroupsKey,
 	}, nil
 }
 
@@ -163,9 +175,12 @@ type oidcConnector struct {
 	insecureSkipEmailVerified bool
 	insecureEnableGroups      bool
 	getUserInfo               bool
+	promptType                string
 	userIDKey                 string
 	userNameKey               string
-	promptType                string
+	preferredUsernameKey      string
+	emailKey                  string
+	groupsKey                 string
 }
 
 func (c *oidcConnector) Close() error {
@@ -273,6 +288,11 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
 		return identity, fmt.Errorf("missing \"%s\" claim", userNameKey)
 	}
 
+	preferredUsername, found := claims["preferred_username"].(string)
+	if !found {
+		preferredUsername, _ = claims[c.preferredUsernameKey].(string)
+	}
+
 	hasEmailScope := false
 	for _, s := range c.oauth2Config.Scopes {
 		if s == "email" {
@@ -281,9 +301,16 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
 		}
 	}
 
-	email, found := claims["email"].(string)
+	var email string
+	emailKey := "email"
+	email, found = claims[emailKey].(string)
+	if !found && c.emailKey != "" {
+		emailKey = c.emailKey
+		email, found = claims[emailKey].(string)
+	}
+
 	if !found && hasEmailScope {
-		return identity, errors.New("missing \"email\" claim")
+		return identity, fmt.Errorf("missing email claim, not found \"%s\" key", emailKey)
 	}
 
 	emailVerified, found := claims["email_verified"].(bool)
@@ -294,8 +321,28 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
 			return identity, errors.New("missing \"email_verified\" claim")
 		}
 	}
-	hostedDomain, _ := claims["hd"].(string)
 
+	var groups []string
+	if c.insecureEnableGroups {
+		groupsKey := "groups"
+		vs, found := claims[groupsKey].([]interface{})
+		if !found {
+			groupsKey = c.groupsKey
+			vs, found = claims[groupsKey].([]interface{})
+		}
+
+		if found {
+			for _, v := range vs {
+				if s, ok := v.(string); ok {
+					groups = append(groups, s)
+				} else {
+					return identity, fmt.Errorf("malformed \"%v\" claim", groupsKey)
+				}
+			}
+		}
+	}
+
+	hostedDomain, _ := claims["hd"].(string)
 	if len(c.hostedDomains) > 0 {
 		found := false
 		for _, domain := range c.hostedDomains {
@@ -320,11 +367,13 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
 	}
 
 	identity = connector.Identity{
-		UserID:        idToken.Subject,
-		Username:      name,
-		Email:         email,
-		EmailVerified: emailVerified,
-		ConnectorData: connData,
+		UserID:            idToken.Subject,
+		Username:          name,
+		PreferredUsername: preferredUsername,
+		Email:             email,
+		EmailVerified:     emailVerified,
+		Groups:            groups,
+		ConnectorData:     connData,
 	}
 
 	if c.userIDKey != "" {
@@ -335,18 +384,5 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
 		identity.UserID = userID
 	}
 
-	if c.insecureEnableGroups {
-		vs, ok := claims["groups"].([]interface{})
-		if ok {
-			for _, v := range vs {
-				if s, ok := v.(string); ok {
-					identity.Groups = append(identity.Groups, s)
-				} else {
-					return identity, errors.New("malformed \"groups\" claim")
-				}
-			}
-		}
-	}
-
 	return identity, nil
 }
diff --git a/connector/oidc/oidc_test.go b/connector/oidc/oidc_test.go
index 52afa158040aef07e03a2500c0cf0dc3ae0bd9b5..ae92f70caa3464506bfed3f0876329ea00e9fa8c 100644
--- a/connector/oidc/oidc_test.go
+++ b/connector/oidc/oidc_test.go
@@ -49,10 +49,15 @@ func TestHandleCallback(t *testing.T) {
 		name                      string
 		userIDKey                 string
 		userNameKey               string
+		preferredUsernameKey      string
+		emailKey                  string
+		groupsKey                 string
 		insecureSkipEmailVerified bool
 		scopes                    []string
 		expectUserID              string
 		expectUserName            string
+		expectGroups              []string
+		expectPreferredUsername   string
 		expectedEmailField        string
 		token                     map[string]interface{}
 	}{
@@ -62,14 +67,31 @@ func TestHandleCallback(t *testing.T) {
 			userNameKey:        "", // not configured
 			expectUserID:       "subvalue",
 			expectUserName:     "namevalue",
+			expectGroups:       []string{"group1", "group2"},
 			expectedEmailField: "emailvalue",
 			token: map[string]interface{}{
 				"sub":            "subvalue",
 				"name":           "namevalue",
+				"groups":         []string{"group1", "group2"},
 				"email":          "emailvalue",
 				"email_verified": true,
 			},
 		},
+		{
+			name:               "customEmailClaim",
+			userIDKey:          "", // not configured
+			userNameKey:        "", // not configured
+			emailKey:           "mail",
+			expectUserID:       "subvalue",
+			expectUserName:     "namevalue",
+			expectedEmailField: "emailvalue",
+			token: map[string]interface{}{
+				"sub":            "subvalue",
+				"name":           "namevalue",
+				"mail":           "emailvalue",
+				"email_verified": true,
+			},
+		},
 		{
 			name:                      "email_verified not in claims, configured to be skipped",
 			insecureSkipEmailVerified: true,
@@ -108,6 +130,48 @@ func TestHandleCallback(t *testing.T) {
 				"email_verified": true,
 			},
 		},
+		{
+			name:                    "withPreferredUsernameKey",
+			preferredUsernameKey:    "username_key",
+			expectUserID:            "subvalue",
+			expectUserName:          "namevalue",
+			expectPreferredUsername: "username_value",
+			expectedEmailField:      "emailvalue",
+			token: map[string]interface{}{
+				"sub":            "subvalue",
+				"name":           "namevalue",
+				"username_key":   "username_value",
+				"email":          "emailvalue",
+				"email_verified": true,
+			},
+		},
+		{
+			name:                    "withoutPreferredUsernameKeyAndBackendReturns",
+			expectUserID:            "subvalue",
+			expectUserName:          "namevalue",
+			expectPreferredUsername: "preferredusernamevalue",
+			expectedEmailField:      "emailvalue",
+			token: map[string]interface{}{
+				"sub":                "subvalue",
+				"name":               "namevalue",
+				"preferred_username": "preferredusernamevalue",
+				"email":              "emailvalue",
+				"email_verified":     true,
+			},
+		},
+		{
+			name:                    "withoutPreferredUsernameKeyAndBackendNotReturn",
+			expectUserID:            "subvalue",
+			expectUserName:          "namevalue",
+			expectPreferredUsername: "",
+			expectedEmailField:      "emailvalue",
+			token: map[string]interface{}{
+				"sub":            "subvalue",
+				"name":           "namevalue",
+				"email":          "emailvalue",
+				"email_verified": true,
+			},
+		},
 		{
 			name:                      "emptyEmailScope",
 			expectUserID:              "subvalue",
@@ -135,6 +199,41 @@ func TestHandleCallback(t *testing.T) {
 				"email":     "emailvalue",
 			},
 		},
+		{
+			name:                      "customGroupsKey",
+			groupsKey:                 "cognito:groups",
+			expectUserID:              "subvalue",
+			expectUserName:            "namevalue",
+			expectedEmailField:        "emailvalue",
+			expectGroups:              []string{"group3", "group4"},
+			scopes:                    []string{"groups"},
+			insecureSkipEmailVerified: true,
+			token: map[string]interface{}{
+				"sub":            "subvalue",
+				"name":           "namevalue",
+				"user_name":      "username",
+				"email":          "emailvalue",
+				"cognito:groups": []string{"group3", "group4"},
+			},
+		},
+		{
+			name:                      "customGroupsKeyButGroupsProvided",
+			groupsKey:                 "cognito:groups",
+			expectUserID:              "subvalue",
+			expectUserName:            "namevalue",
+			expectedEmailField:        "emailvalue",
+			expectGroups:              []string{"group1", "group2"},
+			scopes:                    []string{"groups"},
+			insecureSkipEmailVerified: true,
+			token: map[string]interface{}{
+				"sub":            "subvalue",
+				"name":           "namevalue",
+				"user_name":      "username",
+				"email":          "emailvalue",
+				"groups":         []string{"group1", "group2"},
+				"cognito:groups": []string{"group3", "group4"},
+			},
+		},
 	}
 
 	for _, tc := range tests {
@@ -162,8 +261,12 @@ func TestHandleCallback(t *testing.T) {
 				UserIDKey:                 tc.userIDKey,
 				UserNameKey:               tc.userNameKey,
 				InsecureSkipEmailVerified: tc.insecureSkipEmailVerified,
+				InsecureEnableGroups:      true,
 				BasicAuthUnsupported:      &basicAuth,
 			}
+			config.ClaimMapping.PreferredUsernameKey = tc.preferredUsernameKey
+			config.ClaimMapping.EmailKey = tc.emailKey
+			config.ClaimMapping.GroupsKey = tc.groupsKey
 
 			conn, err := newConnector(config)
 			if err != nil {
@@ -182,8 +285,10 @@ func TestHandleCallback(t *testing.T) {
 
 			expectEquals(t, identity.UserID, tc.expectUserID)
 			expectEquals(t, identity.Username, tc.expectUserName)
+			expectEquals(t, identity.PreferredUsername, tc.expectPreferredUsername)
 			expectEquals(t, identity.Email, tc.expectedEmailField)
 			expectEquals(t, identity.EmailVerified, true)
+			expectEquals(t, identity.Groups, tc.expectGroups)
 		})
 	}
 }