16171: Configurable "username" OIDC claim key.
authorTom Clegg <tom@tomclegg.ca>
Tue, 9 Jun 2020 14:55:08 +0000 (10:55 -0400)
committerTom Clegg <tom@tomclegg.ca>
Tue, 9 Jun 2020 14:55:08 +0000 (10:55 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@tomclegg.ca>

lib/config/config.default.yml
lib/config/generated_config.go
lib/controller/localdb/login.go
lib/controller/localdb/login_oidc.go
lib/controller/localdb/login_oidc_test.go
sdk/go/arvados/config.go

index 8b54d94c6a34eb3be8b7da03db606420a1f629fa..15ffeaadb306be061e5fd3b64ebadc6c42b4e7b0 100644 (file)
@@ -580,6 +580,11 @@ Clusters:
         # use the empty string "".
         EmailVerifiedClaim: "email_verified"
 
+        # OpenID claim field contianing the user's preferred
+        # username. If empty, use the mailbox part of the user's email
+        # address.
+        UsernameClaim: ""
+
       PAM:
         # (Experimental) Use PAM to authenticate users.
         Enable: false
index e4ad42a084aa6190d0c6eb3e947a494da42ae091..94d7f74dd28eebadeec2a4a0b92b2e8e534fd04c 100644 (file)
@@ -586,6 +586,11 @@ Clusters:
         # use the empty string "".
         EmailVerifiedClaim: "email_verified"
 
+        # OpenID claim field contianing the user's preferred
+        # username. If empty, use the mailbox part of the user's email
+        # address.
+        UsernameClaim: ""
+
       PAM:
         # (Experimental) Use PAM to authenticate users.
         Enable: false
index adc22c7e55023943da2b7f339c2dcaa79af97b07..905cfed15c500d95689857e36c1c3323165c4d3d 100644 (file)
@@ -49,6 +49,7 @@ func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) log
                        ClientSecret:       cluster.Login.OpenIDConnect.ClientSecret,
                        EmailClaim:         cluster.Login.OpenIDConnect.EmailClaim,
                        EmailVerifiedClaim: cluster.Login.OpenIDConnect.EmailVerifiedClaim,
+                       UsernameClaim:      cluster.Login.OpenIDConnect.UsernameClaim,
                }
        case !wantGoogle && !wantOpenIDConnect && wantSSO && !wantPAM && !wantLDAP:
                return &ssoLoginController{railsProxy}
index a9047995c15809a2a78685e45fdd7fffb2804d1f..9274d75d7c9fdc1973cbcad621b306599e571893 100644 (file)
@@ -39,6 +39,7 @@ type oidcLoginController struct {
        UseGooglePeopleAPI bool   // Use Google People API to look up alternate email addresses
        EmailClaim         string // OpenID claim to use as email address; typically "email"
        EmailVerifiedClaim string // If non-empty, ensure claim value is true before accepting EmailClaim; typically "email_verified"
+       UsernameClaim      string // If non-empty, use as preferred username
 
        // override Google People API base URL for testing purposes
        // (normally empty, set by google pkg to
@@ -163,6 +164,10 @@ func (ctrl *oidcLoginController) getAuthInfo(ctx context.Context, token *oauth2.
                ret.Email, _ = claims[ctrl.EmailClaim].(string)
        }
 
+       if ctrl.UsernameClaim != "" {
+               ret.Username, _ = claims[ctrl.UsernameClaim].(string)
+       }
+
        if !ctrl.UseGooglePeopleAPI {
                if ret.Email == "" {
                        return nil, fmt.Errorf("cannot log in with unverified email address %q", claims[ctrl.EmailClaim])
@@ -219,9 +224,13 @@ func (ctrl *oidcLoginController) getAuthInfo(ctx context.Context, token *oauth2.
                return nil, errors.New("cannot log in without a verified email address")
        }
        for ae := range altEmails {
-               if ae != ret.Email {
-                       ret.AlternateEmails = append(ret.AlternateEmails, ae)
-                       if i := strings.Index(ae, "@"); i > 0 && strings.ToLower(ae[i+1:]) == strings.ToLower(ctrl.Cluster.Users.PreferDomainForUsername) {
+               if ae == ret.Email {
+                       continue
+               }
+               ret.AlternateEmails = append(ret.AlternateEmails, ae)
+               if ret.Username == "" {
+                       i := strings.Index(ae, "@")
+                       if i > 0 && strings.ToLower(ae[i+1:]) == strings.ToLower(ctrl.Cluster.Users.PreferDomainForUsername) {
                                ret.Username = strings.SplitN(ae[:i], "+", 2)[0]
                        }
                }
index abbd4b98f6af706f2b0463e1c5c82f595480476b..1345e86900dd1056da5a9259bf0d5caf179253e5 100644 (file)
@@ -115,6 +115,7 @@ func (s *OIDCLoginSuite) SetUpTest(c *check.C) {
                                "name":           s.authName,
                                "alt_verified":   true,                    // for custom claim tests
                                "alt_email":      "alt_email@example.com", // for custom claim tests
+                               "alt_username":   "desired-username",      // for custom claim tests
                        })
                        json.NewEncoder(w).Encode(struct {
                                AccessToken  string `json:"access_token"`
@@ -319,7 +320,9 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
                                c.Log("=== succeed because email_verified is false but not required")
                                s.authEmail = "user@oidc.example.com"
                                s.authEmailVerified = false
+                               s.cluster.Login.OpenIDConnect.EmailClaim = "email"
                                s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = ""
+                               s.cluster.Login.OpenIDConnect.UsernameClaim = ""
                        },
                },
                {
@@ -328,7 +331,9 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
                                c.Log("=== fail because email_verified is false and required")
                                s.authEmail = "user@oidc.example.com"
                                s.authEmailVerified = false
+                               s.cluster.Login.OpenIDConnect.EmailClaim = "email"
                                s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = "email_verified"
+                               s.cluster.Login.OpenIDConnect.UsernameClaim = ""
                        },
                },
                {
@@ -337,7 +342,9 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
                                c.Log("=== succeed because email_verified is false but config uses custom 'verified' claim")
                                s.authEmail = "user@oidc.example.com"
                                s.authEmailVerified = false
+                               s.cluster.Login.OpenIDConnect.EmailClaim = "email"
                                s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = "alt_verified"
+                               s.cluster.Login.OpenIDConnect.UsernameClaim = ""
                        },
                },
                {
@@ -348,6 +355,7 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
                                s.authEmailVerified = false
                                s.cluster.Login.OpenIDConnect.EmailClaim = "alt_email"
                                s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = "alt_verified"
+                               s.cluster.Login.OpenIDConnect.UsernameClaim = "alt_username"
                        },
                },
        } {
@@ -377,6 +385,15 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
                c.Check(token, check.Matches, `v2/zzzzz-gj3su-.{15}/.{32,50}`)
                authinfo := getCallbackAuthInfo(c, s.railsSpy)
                c.Check(authinfo.Email, check.Equals, trial.expectEmail)
+
+               switch s.cluster.Login.OpenIDConnect.UsernameClaim {
+               case "alt_username":
+                       c.Check(authinfo.Username, check.Equals, "desired-username")
+               case "":
+                       c.Check(authinfo.Username, check.Equals, "")
+               default:
+                       c.Fail() // bad test case
+               }
        }
 }
 
index 7b07175f1783553e937b8f2c1e3cc8fc170ce9b8..029e223218b2a5136b8eac2238b088e2ce4fb983 100644 (file)
@@ -163,6 +163,7 @@ type Cluster struct {
                        ClientSecret       string
                        EmailClaim         string
                        EmailVerifiedClaim string
+                       UsernameClaim      string
                }
                PAM struct {
                        Enable             bool