Repository: argoproj/argo (go)

Issue:
Use User Email as Subject in Google OIDC Login
# Summary

When using SSO auth mode with Google's OIDC, the issuer identifier (subject) is a unique user ID that isn't very useful and is hard to use for identifying users and managing RBAC permissions in a scalable way.

It will be more useful to use the user's email as the subject when using google as the OIDC provider.
per Google's [OIDC documentation](https://developers.google.com/identity/protocols/oauth2/openid-connect):

```
{
  "iss": "https://accounts.google.com",
  "azp": "1234987819200.apps.googleusercontent.com",
  "aud": "1234987819200.apps.googleusercontent.com",
  "sub": "10769150350006150715113082367",
  "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
  "hd": "example.com",
  "email": "jsmith@example.com",
  "email_verified": "true",
  "iat": 1353601026,
  "exp": 1353604926,
  "nonce": "0394852-3190485-2490358"
}
```

# Use Cases

When google is set as the OIDC provider (issuer `https://accounts.google.com`). This is very useful for teams using G suite.

---
<!-- Issue Author: Don't delete this message to encourage other users to support your issue! -->
**Message from the maintainers**:

Impacted by this bug? Give it a 👍. We prioritise the issues with the most 👍.

Pull request description:
feat(server): Support email for SSO+RBAC. Closes #4612
Signed-off-by: Alex Collins <alex_collins@intuit.com>
Closes #4612

Checklist:

* [x] My organization is added to [USERS.md](https://github.com/argoproj/argo/blob/master/USERS.md).

<!--

Please leave your PR in draft if you don't need a review yet. 

To fix failing `CI / Codegen`, run `make codegen`. 

-->
![Screenshot 2020-12-03 at 12 46 08](https://user-images.githubusercontent.com/1142830/101087106-a2406e80-3566-11eb-86d5-fa4afcedc217.png)


Patch:
diff --git a/api/jsonschema/schema.json b/api/jsonschema/schema.json
index d4092376e..9624b33c0 100644
--- a/api/jsonschema/schema.json
+++ b/api/jsonschema/schema.json
@@ -760,6 +760,13 @@
     },
     "io.argoproj.workflow.v1alpha1.GetUserInfoResponse": {
       "properties": {
+        "email": {
+          "type": "string"
+        },
+        "emailVerified": {
+          "format": "boolean",
+          "type": "boolean"
+        },
         "groups": {
           "items": {
             "type": "string"
diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json
index f2f327f01..f1585f8e6 100644
--- a/api/openapi-spec/swagger.json
+++ b/api/openapi-spec/swagger.json
@@ -2813,6 +2813,13 @@
     "io.argoproj.workflow.v1alpha1.GetUserInfoResponse": {
       "type": "object",
       "properties": {
+        "email": {
+          "type": "string"
+        },
+        "emailVerified": {
+          "type": "boolean",
+          "format": "boolean"
+        },
         "groups": {
           "type": "array",
           "items": {
diff --git a/docs/workflow-controller-configmap.yaml b/docs/workflow-controller-configmap.yaml
index 86f4933bc..bcbd9809b 100644
--- a/docs/workflow-controller-configmap.yaml
+++ b/docs/workflow-controller-configmap.yaml
@@ -239,6 +239,7 @@ data:
       # Additional scopes to request. Typically needed for SSO RBAC. >= v2.12
       scopes:
        - groups
+       - email
       # RBAC Config. >= v2.12
       rbac:
         enabled: false
diff --git a/docs/workflow-creator.md b/docs/workflow-creator.md
index ce23c2b4d..bc5809d4a 100644
--- a/docs/workflow-creator.md
+++ b/docs/workflow-creator.md
@@ -13,6 +13,8 @@ metadata:
   name: my-wf
   labels:
     workflows.argoproj.io/creator: admin
+    # labels must be DNS formatted, so the "@" is replaces by '.at.'  
+    workflows.argoproj.io/creator-email: admin.at.your.org
 ``` 
 
 !!! NOTE
diff --git a/manifests/quick-start/sso/overlays/argo-server-sa.yaml b/manifests/quick-start/sso/overlays/argo-server-sa.yaml
index 462740824..0cd3393f2 100644
--- a/manifests/quick-start/sso/overlays/argo-server-sa.yaml
+++ b/manifests/quick-start/sso/overlays/argo-server-sa.yaml
@@ -3,5 +3,5 @@ kind: ServiceAccount
 metadata:
   name: argo-server
   annotations:
-    workflows.argoproj.io/rbac-rule: "'authors' in groups"
+    workflows.argoproj.io/rbac-rule: "'authors' in groups && email == 'kilgore@kilgore.trout'"
     workflows.argoproj.io/rbac-rule-precedence: "1"
diff --git a/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml b/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml
index 880224298..b502c7f7c 100644
--- a/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml
+++ b/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml
@@ -11,6 +11,7 @@ data:
     redirectUrl: http://localhost:2746/oauth2/callback
     scopes:
     - groups
+    - email
     rbac:
       enabled: true
 kind: ConfigMap
diff --git a/pkg/apiclient/info/info.pb.go b/pkg/apiclient/info/info.pb.go
index 928689ee5..dd95cc02f 100644
--- a/pkg/apiclient/info/info.pb.go
+++ b/pkg/apiclient/info/info.pb.go
@@ -204,6 +204,8 @@ type GetUserInfoResponse struct {
 	Issuer               string   `protobuf:"bytes,1,opt,name=issuer,proto3" json:"issuer,omitempty"`
 	Subject              string   `protobuf:"bytes,2,opt,name=subject,proto3" json:"subject,omitempty"`
 	Groups               []string `protobuf:"bytes,3,rep,name=groups,proto3" json:"groups,omitempty"`
+	Email                string   `protobuf:"bytes,4,opt,name=email,proto3" json:"email,omitempty"`
+	EmailVerified        bool     `protobuf:"varint,5,opt,name=emailVerified,proto3" json:"emailVerified,omitempty"`
 	XXX_NoUnkeyedLiteral struct{} `json:"-"`
 	XXX_unrecognized     []byte   `json:"-"`
 	XXX_sizecache        int32    `json:"-"`
@@ -263,6 +265,20 @@ func (m *GetUserInfoResponse) GetGroups() []string {
 	return nil
 }
 
+func (m *GetUserInfoResponse) GetEmail() string {
+	if m != nil {
+		return m.Email
+	}
+	return ""
+}
+
+func (m *GetUserInfoResponse) GetEmailVerified() bool {
+	if m != nil {
+		return m.EmailVerified
+	}
+	return false
+}
+
 func init() {
 	proto.RegisterType((*GetInfoRequest)(nil), "info.GetInfoRequest")
 	proto.RegisterType((*InfoResponse)(nil), "info.InfoResponse")
@@ -274,36 +290,38 @@ func init() {
 func init() { proto.RegisterFile("pkg/apiclient/info/info.proto", fileDescriptor_96940c93018255fa) }
 
 var fileDescriptor_96940c93018255fa = []byte{
-	// 451 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x93, 0x41, 0x6b, 0x13, 0x41,
-	0x14, 0xc7, 0xd9, 0x44, 0x5b, 0x9c, 0x94, 0x9a, 0x4e, 0x83, 0x5d, 0x17, 0x0d, 0x61, 0x4f, 0x45,
-	0x61, 0x96, 0xd4, 0x53, 0xc1, 0x93, 0x1e, 0x42, 0x41, 0x14, 0x22, 0x7a, 0x10, 0x41, 0x26, 0xdb,
-	0x97, 0xe9, 0x74, 0x37, 0xf3, 0xc6, 0x99, 0xd9, 0xf4, 0xe2, 0xc9, 0xab, 0x47, 0xbf, 0x8a, 0x1f,
-	0xc2, 0xa3, 0xe0, 0x17, 0x90, 0xe0, 0x07, 0x91, 0x9d, 0x9d, 0x6d, 0x12, 0x15, 0x04, 0x2f, 0xcb,
-	0x7b, 0x6f, 0xde, 0xfc, 0xdf, 0xef, 0xbd, 0x37, 0x4b, 0xee, 0xeb, 0x42, 0x64, 0x5c, 0xcb, 0xbc,
-	0x94, 0xa0, 0x5c, 0x26, 0xd5, 0x1c, 0xfd, 0x87, 0x69, 0x83, 0x0e, 0xe9, 0x8d, 0xda, 0x4e, 0xee,
-	0x09, 0x44, 0x51, 0x42, 0x9d, 0x97, 0x71, 0xa5, 0xd0, 0x71, 0x27, 0x51, 0xd9, 0x26, 0x27, 0x79,
-	0x2a, 0xa4, 0xbb, 0xa8, 0x66, 0x2c, 0xc7, 0x45, 0xc6, 0x8d, 0x40, 0x6d, 0xf0, 0xd2, 0x1b, 0x59,
-	0xd0, 0xb6, 0xd9, 0x15, 0x9a, 0x62, 0x5e, 0xe2, 0x55, 0xb6, 0x1c, 0xf3, 0x52, 0x5f, 0xf0, 0x71,
-	0x26, 0x40, 0x81, 0xe1, 0x0e, 0xce, 0x1b, 0x91, 0xb4, 0x4f, 0xf6, 0x27, 0xe0, 0xce, 0xd4, 0x1c,
-	0xa7, 0xf0, 0xbe, 0x02, 0xeb, 0xd2, 0x4f, 0x11, 0xd9, 0x6b, 0x7c, 0xab, 0x51, 0x59, 0xa0, 0x0f,
-	0x48, 0x7f, 0xc1, 0x15, 0x17, 0x70, 0xfe, 0x9c, 0x2f, 0xc0, 0x6a, 0x9e, 0x43, 0x1c, 0x8d, 0xa2,
-	0xe3, 0x5b, 0xd3, 0x3f, 0xe2, 0xf4, 0x05, 0xb9, 0x59, 0x4a, 0x55, 0xd8, 0xb8, 0x33, 0xea, 0x1e,
-	0xf7, 0x4e, 0x4e, 0xd9, 0x9a, 0x91, 0xb5, 0x8c, 0xde, 0x60, 0xba, 0x10, 0xac, 0x66, 0x64, 0x2d,
-	0x23, 0x6b, 0x19, 0xd9, 0x33, 0xa9, 0x8a, 0x69, 0xa3, 0x93, 0x1e, 0x92, 0x83, 0x09, 0xb8, 0xd7,
-	0x60, 0xac, 0x44, 0xd5, 0x22, 0x0e, 0x08, 0x9d, 0x80, 0x7b, 0x65, 0xc1, 0x6c, 0x82, 0xbf, 0x23,
-	0x87, 0x5b, 0xd1, 0x80, 0x7f, 0x87, 0xec, 0x48, 0x6b, 0x2b, 0x30, 0x01, 0x3a, 0x78, 0x34, 0x26,
-	0xbb, 0xb6, 0x9a, 0x5d, 0x42, 0xee, 0xe2, 0x8e, 0x3f, 0x68, 0xdd, 0xfa, 0x86, 0x30, 0x58, 0x69,
-	0x1b, 0x77, 0x47, 0xdd, 0xfa, 0x46, 0xe3, 0x9d, 0x7c, 0xe9, 0x90, 0x5e, 0x2d, 0xfd, 0x12, 0xcc,
-	0x52, 0xe6, 0x40, 0xcf, 0xc8, 0x6e, 0x98, 0x1d, 0x1d, 0x30, 0xbf, 0xbc, 0xed, 0x51, 0x26, 0xb4,
-	0x89, 0x6e, 0xe2, 0xa4, 0x83, 0x8f, 0xdf, 0x7f, 0x7e, 0xee, 0xec, 0xd3, 0x3d, 0xbf, 0xd5, 0xe5,
-	0xd8, 0x6f, 0x9d, 0x7e, 0x20, 0x64, 0xdd, 0x26, 0x3d, 0xba, 0x56, 0xdb, 0x6e, 0x3c, 0x79, 0xfc,
-	0x5f, 0xf3, 0x0c, 0x22, 0xe9, 0x91, 0x2f, 0x7d, 0x40, 0x6f, 0xb7, 0xa5, 0x97, 0xa1, 0xde, 0x5b,
-	0xd2, 0xdb, 0x98, 0x1c, 0x8d, 0xaf, 0xcb, 0xff, 0x36, 0xe2, 0xe4, 0xee, 0x5f, 0x4e, 0x42, 0x5f,
-	0xb1, 0x17, 0xa7, 0xb4, 0xdf, 0x8a, 0x57, 0x16, 0x4c, 0x9d, 0xfd, 0xe4, 0xf4, 0xeb, 0x6a, 0x18,
-	0x7d, 0x5b, 0x0d, 0xa3, 0x1f, 0xab, 0x61, 0xf4, 0xe6, 0xe1, 0xbf, 0x5e, 0xed, 0xc6, 0x1f, 0x31,
-	0xdb, 0xf1, 0x8f, 0xf4, 0xd1, 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x87, 0x05, 0x3d, 0x6e, 0x2e,
-	0x03, 0x00, 0x00,
+	// 484 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x93, 0x41, 0x6f, 0xd3, 0x30,
+	0x14, 0xc7, 0x95, 0x76, 0xdd, 0xc0, 0x1d, 0xa3, 0xf3, 0x2a, 0x66, 0x22, 0xa8, 0xaa, 0x88, 0x43,
+	0x05, 0x92, 0xa3, 0x8e, 0xd3, 0x24, 0x4e, 0x70, 0xa8, 0x26, 0x21, 0x90, 0x8a, 0xd8, 0x01, 0x71,
+	0x71, 0xd3, 0x57, 0xcf, 0x4b, 0x6a, 0x1b, 0xdb, 0xe9, 0x2e, 0x9c, 0xb8, 0x72, 0xe4, 0xc8, 0xd7,
+	0xe0, 0x43, 0x70, 0x44, 0xe2, 0x0b, 0xa0, 0x8a, 0x0f, 0x82, 0xe2, 0x24, 0x6b, 0x03, 0x48, 0x48,
+	0x5c, 0xa2, 0xf7, 0x9e, 0x9f, 0xff, 0xef, 0xe7, 0xf7, 0x5e, 0xd0, 0x7d, 0x9d, 0xf2, 0x98, 0x69,
+	0x91, 0x64, 0x02, 0xa4, 0x8b, 0x85, 0x5c, 0x28, 0xff, 0xa1, 0xda, 0x28, 0xa7, 0xf0, 0x4e, 0x61,
+	0x87, 0xf7, 0xb8, 0x52, 0x3c, 0x83, 0x22, 0x2f, 0x66, 0x52, 0x2a, 0xc7, 0x9c, 0x50, 0xd2, 0x96,
+	0x39, 0xe1, 0x33, 0x2e, 0xdc, 0x45, 0x3e, 0xa3, 0x89, 0x5a, 0xc6, 0xcc, 0x70, 0xa5, 0x8d, 0xba,
+	0xf4, 0x46, 0x5c, 0x69, 0xdb, 0xf8, 0x4a, 0x99, 0x74, 0x91, 0xa9, 0xab, 0x78, 0x35, 0x66, 0x99,
+	0xbe, 0x60, 0xe3, 0x98, 0x83, 0x04, 0xc3, 0x1c, 0xcc, 0x4b, 0x91, 0xa8, 0x87, 0x0e, 0x26, 0xe0,
+	0xce, 0xe4, 0x42, 0x4d, 0xe1, 0x5d, 0x0e, 0xd6, 0x45, 0x1f, 0x03, 0xb4, 0x5f, 0xfa, 0x56, 0x2b,
+	0x69, 0x01, 0x3f, 0x44, 0xbd, 0x25, 0x93, 0x8c, 0xc3, 0xfc, 0x05, 0x5b, 0x82, 0xd5, 0x2c, 0x01,
+	0x12, 0x0c, 0x83, 0xd1, 0xcd, 0xe9, 0x1f, 0x71, 0xfc, 0x12, 0x75, 0x32, 0x21, 0x53, 0x4b, 0x5a,
+	0xc3, 0xf6, 0xa8, 0x7b, 0x72, 0x4a, 0x37, 0x8c, 0xb4, 0x66, 0xf4, 0x06, 0xd5, 0x29, 0xa7, 0x05,
+	0x23, 0xad, 0x19, 0x69, 0xcd, 0x48, 0x9f, 0x0b, 0x99, 0x4e, 0x4b, 0x9d, 0xe8, 0x08, 0x1d, 0x4e,
+	0xc0, 0x9d, 0x83, 0xb1, 0x42, 0xc9, 0x1a, 0xb1, 0x8f, 0xf0, 0x04, 0xdc, 0x6b, 0x0b, 0x66, 0x1b,
+	0xfc, 0x73, 0x80, 0x8e, 0x1a, 0xe1, 0x8a, 0xff, 0x0e, 0xda, 0x15, 0xd6, 0xe6, 0x60, 0x2a, 0xea,
+	0xca, 0xc3, 0x04, 0xed, 0xd9, 0x7c, 0x76, 0x09, 0x89, 0x23, 0x2d, 0x7f, 0x50, 0xbb, 0xc5, 0x0d,
+	0x6e, 0x54, 0xae, 0x2d, 0x69, 0x0f, 0xdb, 0xc5, 0x8d, 0xd2, 0xc3, 0x7d, 0xd4, 0x81, 0x25, 0x13,
+	0x19, 0xd9, 0xf1, 0xf9, 0xa5, 0x83, 0x1f, 0xa0, 0x5b, 0xde, 0x38, 0x07, 0x23, 0x16, 0x02, 0xe6,
+	0xa4, 0x33, 0x0c, 0x46, 0x37, 0xa6, 0xcd, 0xe0, 0xc9, 0x97, 0x16, 0xea, 0x16, 0x58, 0xaf, 0xc0,
+	0xac, 0x44, 0x02, 0xf8, 0x0c, 0xed, 0x55, 0x8d, 0xc7, 0x7d, 0xea, 0x27, 0xdf, 0x9c, 0x43, 0x88,
+	0xcb, 0xe8, 0xf6, 0x53, 0xa2, 0xfe, 0x87, 0xef, 0x3f, 0x3f, 0xb5, 0x0e, 0xf0, 0xbe, 0x5f, 0x89,
+	0xd5, 0xd8, 0xaf, 0x0c, 0x7e, 0x8f, 0xd0, 0xa6, 0x47, 0xf8, 0xf8, 0x5a, 0xad, 0xd9, 0xb5, 0xf0,
+	0xc9, 0x7f, 0x0d, 0xa3, 0x12, 0x89, 0x8e, 0x7d, 0xe9, 0x43, 0x7c, 0xbb, 0x2e, 0xbd, 0xaa, 0xea,
+	0xbd, 0x45, 0xdd, 0xad, 0xae, 0x63, 0x72, 0x5d, 0xfe, 0xb7, 0xf9, 0x84, 0x77, 0xff, 0x72, 0x52,
+	0xbd, 0x8b, 0x78, 0x71, 0x8c, 0x7b, 0xb5, 0x78, 0x6e, 0xc1, 0x14, 0xd9, 0x4f, 0x4f, 0xbf, 0xae,
+	0x07, 0xc1, 0xb7, 0xf5, 0x20, 0xf8, 0xb1, 0x1e, 0x04, 0x6f, 0x1e, 0xfd, 0x6b, 0xe5, 0xb7, 0x7e,
+	0xa7, 0xd9, 0xae, 0xdf, 0xf0, 0xc7, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0xff, 0x44, 0x46, 0x13,
+	0x6b, 0x03, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -611,6 +629,23 @@ func (m *GetUserInfoResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 		i -= len(m.XXX_unrecognized)
 		copy(dAtA[i:], m.XXX_unrecognized)
 	}
+	if m.EmailVerified {
+		i--
+		if m.EmailVerified {
+			dAtA[i] = 1
+		} else {
+			dAtA[i] = 0
+		}
+		i--
+		dAtA[i] = 0x28
+	}
+	if len(m.Email) > 0 {
+		i -= len(m.Email)
+		copy(dAtA[i:], m.Email)
+		i = encodeVarintInfo(dAtA, i, uint64(len(m.Email)))
+		i--
+		dAtA[i] = 0x22
+	}
 	if len(m.Groups) > 0 {
 		for iNdEx := len(m.Groups) - 1; iNdEx >= 0; iNdEx-- {
 			i -= len(m.Groups[iNdEx])
@@ -726,6 +761,13 @@ func (m *GetUserInfoResponse) Size() (n int) {
 			n += 1 + l + sovInfo(uint64(l))
 		}
 	}
+	l = len(m.Email)
+	if l > 0 {
+		n += 1 + l + sovInfo(uint64(l))
+	}
+	if m.EmailVerified {
+		n += 2
+	}
 	if m.XXX_unrecognized != nil {
 		n += len(m.XXX_unrecognized)
 	}
@@ -1145,6 +1187,58 @@ func (m *GetUserInfoResponse) Unmarshal(dAtA []byte) error {
 			}
 			m.Groups = append(m.Groups, string(dAtA[iNdEx:postIndex]))
 			iNdEx = postIndex
+		case 4:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Email", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowInfo
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthInfo
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthInfo
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Email = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 5:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field EmailVerified", wireType)
+			}
+			var v int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowInfo
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.EmailVerified = bool(v != 0)
 		default:
 			iNdEx = preIndex
 			skippy, err := skipInfo(dAtA[iNdEx:])
diff --git a/pkg/apiclient/info/info.proto b/pkg/apiclient/info/info.proto
index a0a2dc9fc..3d55b088a 100644
--- a/pkg/apiclient/info/info.proto
+++ b/pkg/apiclient/info/info.proto
@@ -24,6 +24,8 @@ message GetUserInfoResponse {
     string issuer = 1;
     string subject = 2;
     repeated string groups = 3;
+    string email = 4;
+    bool emailVerified = 5;
 }
 
 service InfoService {
diff --git a/server/auth/sso/sso.go b/server/auth/sso/sso.go
index 70cd93e60..47c0b7ae8 100644
--- a/server/auth/sso/sso.go
+++ b/server/auth/sso/sso.go
@@ -172,7 +172,7 @@ func newSso(
 	if err != nil {
 		return nil, fmt.Errorf("failed to create JWT encrpytor: %w", err)
 	}
-	log.WithFields(log.Fields{"redirectUrl": config.RedirectURL, "issuer": c.Issuer, "clientId": c.ClientID}).Info("SSO configuration")
+	log.WithFields(log.Fields{"redirectUrl": config.RedirectURL, "issuer": c.Issuer, "clientId": c.ClientID, "scopes": config.Scopes}).Info("SSO configuration")
 	return &sso{
 		config:          config,
 		idTokenVerifier: idTokenVerifier,
@@ -233,7 +233,14 @@ func (s *sso) HandleCallback(w http.ResponseWriter, r *http.Request) {
 		_, _ = w.Write([]byte(fmt.Sprintf("failed to get claims: %v", err)))
 		return
 	}
-	argoClaims := &types.Claims{Claims: jwt.Claims{Issuer: issuer, Subject: c.Subject, Expiry: jwt.NewNumericDate(time.Now().Add(s.expiry))}, Groups: c.Groups}
+	argoClaims := &types.Claims{Claims: jwt.Claims{
+		Issuer:  issuer,
+		Subject: c.Subject,
+		Expiry:  jwt.NewNumericDate(time.Now().Add(s.expiry))},
+		Groups:        c.Groups,
+		Email:         c.Email,
+		EmailVerified: c.EmailVerified,
+	}
 	raw, err := jwt.Encrypted(s.encrypter).Claims(argoClaims).CompactSerialize()
 	if err != nil {
 		panic(err)
diff --git a/server/auth/types/claims.go b/server/auth/types/claims.go
index 7b8c55e41..5bcda24e6 100644
--- a/server/auth/types/claims.go
+++ b/server/auth/types/claims.go
@@ -4,5 +4,7 @@ import "gopkg.in/square/go-jose.v2/jwt"
 
 type Claims struct {
 	jwt.Claims
-	Groups []string `json:"groups,omitempty"`
+	Groups        []string `json:"groups,omitempty"`
+	Email         string   `json:"email,omitempty"`
+	EmailVerified bool     `json:"email_verified,omitempty"`
 }
diff --git a/server/info/info_server.go b/server/info/info_server.go
index 57ba9a584..759e9dac0 100644
--- a/server/info/info_server.go
+++ b/server/info/info_server.go
@@ -17,7 +17,13 @@ type infoServer struct {
 func (i *infoServer) GetUserInfo(ctx context.Context, _ *infopkg.GetUserInfoRequest) (*infopkg.GetUserInfoResponse, error) {
 	claims := auth.GetClaims(ctx)
 	if claims != nil {
-		return &infopkg.GetUserInfoResponse{Subject: claims.Subject, Issuer: claims.Issuer, Groups: claims.Groups}, nil
+		return &infopkg.GetUserInfoResponse{
+			Subject:       claims.Subject,
+			Issuer:        claims.Issuer,
+			Groups:        claims.Groups,
+			Email:         claims.Email,
+			EmailVerified: claims.EmailVerified,
+		}, nil
 	}
 	return &infopkg.GetUserInfoResponse{}, nil
 }
diff --git a/ui/src/app/userinfo/components/user-info.tsx b/ui/src/app/userinfo/components/user-info.tsx
index 76854ae22..626098dda 100644
--- a/ui/src/app/userinfo/components/user-info.tsx
+++ b/ui/src/app/userinfo/components/user-info.tsx
@@ -40,6 +40,8 @@ export class UserInfo extends BasePage<RouteComponentProps<any>, State> {
                                 <p>Issuer: {this.state.userInfo.issuer || '-'}</p>
                                 <p>Subject: {this.state.userInfo.subject || '-'}</p>
                                 <p>Groups: {(this.state.userInfo.groups && this.state.userInfo.groups.length > 0 && this.state.userInfo.groups.join(', ')) || '-'}</p>
+                                <p>Email: {this.state.userInfo.email || '-'}</p>
+                                <p>Email Verified: {this.state.userInfo.emailVerified || '-'}</p>
                             </>
                         )}
                         <a className='argo-button argo-button--base-o' href={uiUrl('login')}>
diff --git a/ui/src/models/info.ts b/ui/src/models/info.ts
index 84927f32a..fe754c4c0 100644
--- a/ui/src/models/info.ts
+++ b/ui/src/models/info.ts
@@ -17,4 +17,6 @@ export interface GetUserInfoResponse {
     subject?: string;
     issuer?: string;
     groups?: string[];
+    email?: string;
+    emailVerified?: boolean;
 }
diff --git a/workflow/common/common.go b/workflow/common/common.go
index df3ff82fd..cf47cdc20 100644
--- a/workflow/common/common.go
+++ b/workflow/common/common.go
@@ -55,7 +55,8 @@ const (
 	// for the purposes of workflow segregation
 	LabelKeyControllerInstanceID = workflow.WorkflowFullName + "/controller-instanceid"
 	// Who created this workflow.
-	LabelKeyCreator = workflow.WorkflowFullName + "/creator"
+	LabelKeyCreator      = workflow.WorkflowFullName + "/creator"
+	LabelKeyCreatorEmail = workflow.WorkflowFullName + "/creator-email"
 	// LabelKeyCompleted is the metadata label applied on worfklows and workflow pods to indicates if resource is completed
 	// Workflows and pods with a completed=true label will be ignored by the controller.
 	// See also `LabelKeyWorkflowArchivingStatus`.
diff --git a/workflow/creator/creator.go b/workflow/creator/creator.go
index 5a8ffc4ed..e5d78cc0c 100644
--- a/workflow/creator/creator.go
+++ b/workflow/creator/creator.go
@@ -15,11 +15,18 @@ import (
 func Label(ctx context.Context, obj metav1.Object) {
 	claims := auth.GetClaims(ctx)
 	if claims != nil {
-		value := regexp.MustCompile("[^-_.a-z0-9A-Z]").ReplaceAllString(claims.Subject, "-")
-		if len(value) > 63 {
-			value = value[len(value)-63:]
+		labels.Label(obj, common.LabelKeyCreator, dnsFriendly(claims.Subject))
+		if claims.Email != "" {
+			labels.Label(obj, common.LabelKeyCreatorEmail, dnsFriendly(strings.Replace(claims.Email, "@", ".at.", 1)))
 		}
-		value = strings.TrimLeft(value, "-")
-		labels.Label(obj, common.LabelKeyCreator, value)
 	}
 }
+
+func dnsFriendly(s string) string {
+	value := regexp.MustCompile("[^-_.a-z0-9A-Z]").ReplaceAllString(s, "-")
+	if len(value) > 63 {
+		value = value[len(value)-63:]
+	}
+	value = strings.TrimLeft(value, "-")
+	return value
+}


Test patch:
diff --git a/server/info/info_server_test.go b/server/info/info_server_test.go
index 8e0d9cf04..fc9a77c29 100644
--- a/server/info/info_server_test.go
+++ b/server/info/info_server_test.go
@@ -13,11 +13,13 @@ import (
 
 func Test_infoServer_GetUserInfo(t *testing.T) {
 	i := &infoServer{}
-	ctx := context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Issuer: "my-iss", Subject: "my-sub"}, Groups: []string{"my-group"}})
+	ctx := context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Issuer: "my-iss", Subject: "my-sub"}, Groups: []string{"my-group"}, Email: "my@email", EmailVerified: true})
 	info, err := i.GetUserInfo(ctx, nil)
 	if assert.NoError(t, err) {
 		assert.Equal(t, "my-iss", info.Issuer)
 		assert.Equal(t, "my-sub", info.Subject)
 		assert.Equal(t, []string{"my-group"}, info.Groups)
+		assert.Equal(t, "my@email", info.Email)
+		assert.True(t, info.EmailVerified)
 	}
 }
diff --git a/workflow/creator/creator_test.go b/workflow/creator/creator_test.go
index caf6db199..1559a0b77 100644
--- a/workflow/creator/creator_test.go
+++ b/workflow/creator/creator_test.go
@@ -23,16 +23,10 @@ func TestLabel(t *testing.T) {
 	})
 	t.Run("NotEmpty", func(t *testing.T) {
 		wf := &wfv1.Workflow{}
-		Label(context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: "my-sub"}}), wf)
+		Label(context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: strings.Repeat("x", 63) + "y"}, Email: "my@email"}), wf)
 		if assert.NotEmpty(t, wf.Labels) {
-			assert.Contains(t, wf.Labels, common.LabelKeyCreator)
-		}
-	})
-	t.Run("TooLong", func(t *testing.T) {
-		wf := &wfv1.Workflow{}
-		Label(context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: strings.Repeat("x", 63) + "y"}}), wf)
-		if assert.NotEmpty(t, wf.Labels) {
-			assert.Equal(t, strings.Repeat("x", 62)+"y", wf.Labels[common.LabelKeyCreator])
+			assert.Equal(t, strings.Repeat("x", 62)+"y", wf.Labels[common.LabelKeyCreator], "creator is truncated")
+			assert.Equal(t, "my.at.email", wf.Labels[common.LabelKeyCreatorEmail], "'@' is replaced by '.at.'")
 		}
 	})
 	t.Run("TooLongHyphen", func(t *testing.T) {