diff --git a/Documentation/custom-scopes-claims-clients.md b/Documentation/custom-scopes-claims-clients.md
index 0c2770169806068b72ef0da515d02ee712141a6b..877e7a57629a02e76f8eece98b047f8b8eb2e575 100644
--- a/Documentation/custom-scopes-claims-clients.md
+++ b/Documentation/custom-scopes-claims-clients.md
@@ -12,6 +12,7 @@ The following is the exhaustive list of scopes supported by dex:
 | `email` | ID token claims should include the end user's email and if that email was verified by an upstream provider. |
 | `profile` | ID token claims should include the username of the end user. |
 | `groups` | ID token claims should include a list of groups the end user is a member of. |
+| `federated:id` | ID token claims should include information from the ID provider. The token will contain the connector ID and the user ID assigned at the provider. |
 | `offline_access` | Token response should include a refresh token. Doesn't work in combinations with some connectors, notability the [SAML connector][saml-connector] ignores this scope. |
 | `audience:server:client_id:( client-id )` | Dynamic scope indicating that the ID token should be issued on behalf of another client. See the _"Cross-client trust and authorized party"_ section below. |
 
@@ -22,10 +23,20 @@ Beyond the [required OpenID Connect claims][core-claims], and a handful of [stan
 | Name | Description |
 | ---- | ------------|
 | `groups` | A list of strings representing the groups a user is a member of. |
+| `federated_claims` | The connector ID and the user ID assigned to the user at the provider. |
 | `email` | The email of the user. |
 | `email_verified` | If the upstream provider has verified the email. |
 | `name` | User's display name. |
 
+The `federated_claims` claim has the following format:
+
+```json
+"federated_claims": {
+  "connector_id": "github",
+  "user_id": "110272483197731336751"
+}
+```
+
 ## Cross-client trust and authorized party
 
 Dex has the ability to issue ID tokens to clients on behalf of other clients. In OpenID Connect terms, this means the ID token's `aud` (audience) claim being a different client ID than the client that performed the login.
diff --git a/server/oauth2.go b/server/oauth2.go
index 6a0d5eee55ea56759949ae23452582750922567a..7967b1bca8050f75a95a9887dbcd930c74803bb2 100644
--- a/server/oauth2.go
+++ b/server/oauth2.go
@@ -107,6 +107,7 @@ const (
 	scopeGroups            = "groups"
 	scopeEmail             = "email"
 	scopeProfile           = "profile"
+	scopeFederatedID       = "federated:id"
 	scopeCrossClientPrefix = "audience:server:client_id:"
 )
 
@@ -255,6 +256,13 @@ type idTokenClaims struct {
 	Groups []string `json:"groups,omitempty"`
 
 	Name string `json:"name,omitempty"`
+
+	FederatedIDClaims *federatedIDClaims `json:"federated_claims,omitempty"`
+}
+
+type federatedIDClaims struct {
+	ConnectorID string `json:"connector_id,omitempty"`
+	UserID      string `json:"user_id,omitempty"`
 }
 
 func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []string, nonce, accessToken, connID string) (idToken string, expiry time.Time, err error) {
@@ -313,6 +321,11 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
 			tok.Groups = claims.Groups
 		case scope == scopeProfile:
 			tok.Name = claims.Username
+		case scope == scopeFederatedID:
+			tok.FederatedIDClaims = &federatedIDClaims{
+				ConnectorID: connID,
+				UserID:      claims.UserID,
+			}
 		default:
 			peerID, ok := parseCrossClientScope(scope)
 			if !ok {
@@ -405,7 +418,7 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (req storage.AuthReq
 		switch scope {
 		case scopeOpenID:
 			hasOpenIDScope = true
-		case scopeOfflineAccess, scopeEmail, scopeProfile, scopeGroups:
+		case scopeOfflineAccess, scopeEmail, scopeProfile, scopeGroups, scopeFederatedID:
 		default:
 			peerID, ok := parseCrossClientScope(scope)
 			if !ok {