You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by sr...@apache.org on 2022/03/11 18:16:31 UTC

[trafficcontrol] branch master updated: JWT Authorization (#6577)

This is an automated email from the ASF dual-hosted git repository.

srijeet0406 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new f9a450b  JWT Authorization (#6577)
f9a450b is described below

commit f9a450b6822317e99663b5e92ac0065b351f758a
Author: mattjackson220 <33...@users.noreply.github.com>
AuthorDate: Fri Mar 11 11:16:22 2022 -0700

    JWT Authorization (#6577)
    
    * Added the ability to use a JWT for authorization and added to CDNi operations
    
    * updated changelog
    
    * fixed older versions
    
    * updated per comments
    
    * updated changelog
    
    * updated per comments
    
    * updated to remove authorization header
    
    * updated per comments
    
    * removed autofocus
    
    * fixed logout
    
    * Added ability to use Authorization Header instead of just cookies
    
    * updated per comments
    
    * updated to protect for NPE
    
    * updated per comments
    
    * updated per comments
    
    * fixed test
    
    * fixed after rebase
---
 CHANGELOG.md                                       |  1 +
 docs/source/admin/traffic_ops.rst                  |  1 -
 docs/source/api/v4/oc_ci_configuration.rst         | 24 ++++---
 docs/source/api/v4/oc_ci_configuration_host.rst    | 24 ++++---
 docs/source/api/v4/oc_fci_advertisement.rst        | 26 +++----
 docs/source/api/v4/users.rst                       |  4 +-
 docs/source/api/v4/users_id.rst                    | 18 +++++
 lib/go-rfc/http.go                                 |  1 +
 lib/go-tc/users.go                                 |  1 +
 traffic_ops/app/conf/cdn.conf                      |  3 +-
 ...022021611354000_add_user_to_ucdn_table.down.sql | 18 +++++
 .../2022021611354000_add_user_to_ucdn_table.up.sql | 18 +++++
 traffic_ops/traffic_ops_golang/api/api.go          | 79 ++++++++++++++++++++--
 traffic_ops/traffic_ops_golang/auth/authorize.go   | 22 ++++--
 traffic_ops/traffic_ops_golang/cdni/shared.go      | 59 ++++++++++++----
 traffic_ops/traffic_ops_golang/config/config.go    |  3 +-
 traffic_ops/traffic_ops_golang/login/login.go      | 48 +++++++++++--
 traffic_ops/traffic_ops_golang/login/logout.go     |  9 +++
 .../routing/middleware/wrappers_test.go            |  2 +-
 traffic_ops/traffic_ops_golang/user/user.go        | 14 ++--
 .../common/modules/form/user/form.user.tpl.html    |  8 +++
 traffic_portal/test/integration/Data/users.ts      |  1 +
 .../test/integration/PageObjects/UsersPage.po.ts   |  3 +
 23 files changed, 315 insertions(+), 72 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb316a5..5d31645 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Traffic Monitor config option `distributed_polling` which enables the ability for Traffic Monitor to poll a subset of the CDN and divide into "local peer groups" and "distributed peer groups". Traffic Monitors in the same group are local peers, while Traffic Monitors in other groups are distibuted peers. Each TM group polls the same set of cachegroups and gets availability data for the other cachegroups from other TM groups. This allows each TM to be responsible for polling a subset of [...]
 - Added support for a new Traffic Ops GLOBAL profile parameter -- `tm_query_status_override` -- to override which status of Traffic Monitors to query (default: ONLINE).
 - Traffic Router: Add support for `file`-protocol URLs for the `geolocation.polling.url` for the Geolocation database.
+- Added functionality for login to provide a Bearer token and for that token to be later used for authorization.
 
 ### Fixed
 - Update traffic\_portal dependencies to mitigate `npm audit` issues.
diff --git a/docs/source/admin/traffic_ops.rst b/docs/source/admin/traffic_ops.rst
index 1579ff5..623a630 100644
--- a/docs/source/admin/traffic_ops.rst
+++ b/docs/source/admin/traffic_ops.rst
@@ -500,7 +500,6 @@ This file deals with the configuration parameters of running Traffic Ops itself.
 	.. versionadded:: 6.2
 
 	:dcdn_id: A string representing this :abbr:`CDN (Content Delivery Network)` to be used in the :abbr:`JWT (JSON Web Token)` and subsequently in :abbr:`CDNi (Content Delivery Network Interconnect)` operations.
-	:jwt_decoding_secret: A string used to decode the :abbr:`JWT (JSON Web Token)` to get information for :abbr:`CDNi (Content Delivery Network Interconnect)` operations.
 
 
 Example cdn.conf
diff --git a/docs/source/api/v4/oc_ci_configuration.rst b/docs/source/api/v4/oc_ci_configuration.rst
index 25d9dfc..c497785 100644
--- a/docs/source/api/v4/oc_ci_configuration.rst
+++ b/docs/source/api/v4/oc_ci_configuration.rst
@@ -23,22 +23,28 @@
 =======
 Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
 
+.. note:: Users with the ``ICDN:UCDN-OVERRIDE`` permission will need to provide a "ucdn" query parameter to bypass the need for :abbr:`uCDN (Upstream Content Delivery Network)` information in the :abbr:`JWT (JSON Web Token)` and allow them to view all :abbr:`CDNi (Content Delivery Network Interconnect)` information.
+
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
 :Permissions Required: CDNI:UPDATE
 :Response Type:  Object
-:Headers Required: "Authorization"
 
 Request Structure
 -----------------
-.. table:: Request Required Headers
-
-	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
-	|    Name         | Description                                                                                                                  |
-	+=================+==============================================================================================================================+
-	|  Authorization  | A :abbr:`JWT (JSON Web Token)` provided by the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the            |
-	|                 | :abbr:`uCDN (Upstream Content Delivery Network)`                                                                             |
-	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+This requires authorization using a :abbr:`JWT (JSON Web Token)` provided by the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the :abbr:`uCDN (Upstream Content Delivery Network)`. This token must include the following claims:
+
+.. table:: Required JWT claims
+
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
+	|    Name         | Description                                                                                                        |
+	+=================+====================================================================================================================+
+	|      iss        | Issuer claim as a string key for the :abbr:`uCDN (Upstream Content Delivery Network)`                              |
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
+	|      aud        | Audience claim as a string key for the :abbr:`dCDN (Downstream Content Delivery Network)`                          |
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
+	|      exp        | Expiration claim as the expiration date as a Unix epoch timestamp (in seconds)                                     |
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
 
 :type: A string of the type of metadata to follow. See :rfc:`8006` for possible values. Only a selection of these are supported.
 :host: A string of the domain that the requested updates will change.
diff --git a/docs/source/api/v4/oc_ci_configuration_host.rst b/docs/source/api/v4/oc_ci_configuration_host.rst
index a44ae961..688e2bf 100644
--- a/docs/source/api/v4/oc_ci_configuration_host.rst
+++ b/docs/source/api/v4/oc_ci_configuration_host.rst
@@ -23,22 +23,28 @@
 =======
 Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` and the specified host by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
 
+.. note:: Users with the ``ICDN:UCDN-OVERRIDE`` permission will need to provide a "ucdn" query parameter to bypass the need for :abbr:`uCDN (Upstream Content Delivery Network)` information in the :abbr:`JWT (JSON Web Token)` and allow them to view all :abbr:`CDNi (Content Delivery Network Interconnect)` information.
+
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
 :Permissions Required: CDNI:UPDATE
 :Response Type:  Object
-:Headers Required: "Authorization"
 
 Request Structure
 -----------------
-.. table:: Request Required Headers
-
-	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
-	|    Name         | Description                                                                                                                  |
-	+=================+==============================================================================================================================+
-	|  Authorization  | A :abbr:`JWT (JSON Web Token)` provided by the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the            |
-	|                 | :abbr:`uCDN (Upstream Content Delivery Network)`                                                                             |
-	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+This requires authorization using a :abbr:`JWT (JSON Web Token)` provided by the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the :abbr:`uCDN (Upstream Content Delivery Network)`. This token must include the following claims:
+
+.. table:: Required JWT claims
+
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
+	|    Name         | Description                                                                                                        |
+	+=================+====================================================================================================================+
+	|      iss        | Issuer claim as a string key for the :abbr:`uCDN (Upstream Content Delivery Network)`                              |
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
+	|      aud        | Audience claim as a string key for the :abbr:`dCDN (Downstream Content Delivery Network)`                          |
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
+	|      exp        | Expiration claim as the expiration date as a Unix epoch timestamp (in seconds)                                     |
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
 
 .. table:: Request Path Parameters
 
diff --git a/docs/source/api/v4/oc_fci_advertisement.rst b/docs/source/api/v4/oc_fci_advertisement.rst
index 319c7a9..f58e339 100644
--- a/docs/source/api/v4/oc_fci_advertisement.rst
+++ b/docs/source/api/v4/oc_fci_advertisement.rst
@@ -23,26 +23,28 @@
 =======
 Returns the complete footprint and capabilities information structure the :abbr:`dCDN (Downstream Content Delivery Network)` wants to expose to a given :abbr:`uCDN (Upstream Content Delivery Network)`.
 
+.. note:: Users with the ``ICDN:UCDN-OVERRIDE`` permission will need to provide a "ucdn" query parameter to bypass the need for :abbr:`uCDN (Upstream Content Delivery Network)` information in the :abbr:`JWT (JSON Web Token)` and allow them to view all :abbr:`CDNi (Content Delivery Network Interconnect)` information.
+
 :Auth. Required: No
 :Roles Required: "admin" or "operations"
 :Permissions Required: CDNI:READ
 :Response Type:  Array
-:Headers Required: "Authorization"
 
 Request Structure
 -----------------
-.. table:: Request Required Headers
+This requires authorization using a :abbr:`JWT (JSON Web Token)` provided by the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the :abbr:`uCDN (Upstream Content Delivery Network)`. This token must include the following claims:
+
+.. table:: Required JWT claims
 
-	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
-	|    Name         | Description                                                                                                                  |
-	+=================+==============================================================================================================================+
-	|  Authorization  | A :abbr:`JWT (JSON Web Token)` provided by the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the            |
-	|                 | :abbr:`uCDN (Upstream Content Delivery Network)`. This token must include the following claims:                              |
-	|                 |                                                                                                                              |
-	|                 | - ``iss`` Issuer claim as a string key for the :abbr:`uCDN (Upstream Content Delivery Network)`                              |
-	|                 | - ``aud`` Audience claim as a string key for the :abbr:`dCDN (Downstream Content Delivery Network)`                          |
-	|                 | - ``exp`` Expiration claim as the expiration date as a Unix epoch timestamp (in seconds)                                     |
-	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
+	|    Name         | Description                                                                                                        |
+	+=================+====================================================================================================================+
+	|      iss        | Issuer claim as a string key for the :abbr:`uCDN (Upstream Content Delivery Network)`                              |
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
+	|      aud        | Audience claim as a string key for the :abbr:`dCDN (Downstream Content Delivery Network)`                          |
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
+	|      exp        | Expiration claim as the expiration date as a Unix epoch timestamp (in seconds)                                     |
+	+-----------------+--------------------------------------------------------------------------------------------------------------------+
 
 Response Structure
 ------------------
diff --git a/docs/source/api/v4/users.rst b/docs/source/api/v4/users.rst
index 5df71a1..a10335f 100644
--- a/docs/source/api/v4/users.rst
+++ b/docs/source/api/v4/users.rst
@@ -90,6 +90,7 @@ Response Structure
 :stateOrProvince:   The name of the state or province where this user resides
 :tenant:            The name of the tenant to which this user belongs
 :tenantId:          The integral, unique identifier of the tenant to which this user belongs
+:ucdn:              The name of the :abbr:`uCDN (Upstream Content Delivery Network)` to which the user belongs
 :uid:               A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX user ID of the user - now it is always ``null``
 :username:          The user's username
 
@@ -168,7 +169,8 @@ Request Structure
 
 	.. note:: This field is optional if and only if tenancy is not enabled in Traffic Control
 
-:username: The new user's username
+:ucdn:               An optional field (only used if :abbr:`CDNi (Content Delivery Network Interconnect)` is in use) containing the name of the :abbr:`uCDN (Upstream Content Delivery Network)` to which the user belongs
+:username:           The new user's username
 
 .. code-block:: http
 	:caption: Request Example
diff --git a/docs/source/api/v4/users_id.rst b/docs/source/api/v4/users_id.rst
index a668d70..b9a0ffb 100644
--- a/docs/source/api/v4/users_id.rst
+++ b/docs/source/api/v4/users_id.rst
@@ -69,6 +69,12 @@ Response Structure
 :stateOrProvince:   The name of the state or province where this user resides
 :tenant:            The name of the tenant to which this user belongs
 :tenantId:          The integral, unique identifier of the tenant to which this user belongs
+:ucdn:              The name of the :abbr:`uCDN (Upstream Content Delivery Network)` to which the user belongs
+
+	.. versionadded:: 6.2
+
+	.. note:: This field is optional and only used if :abbr:`CDNi (Content Delivery Network Interconnect)` is in use.
+
 :uid:               A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX user ID of the user - now it is always ``null``
 :username:          The user's username
 
@@ -152,6 +158,12 @@ Request Structure
 
 	.. note:: This field is optional if and only if tenancy is not enabled in Traffic Control
 
+:ucdn:              The name of the :abbr:`uCDN (Upstream Content Delivery Network)` to which the user belongs
+
+	.. versionadded:: 6.2
+
+	.. note:: This field is optional and only used if :abbr:`CDNi (Content Delivery Network Interconnect)` is in use.
+
 :username: The user's username
 
 .. code-block:: http
@@ -203,6 +215,12 @@ Response Structure
 :stateOrProvince:  The name of the state or province where this user resides
 :tenant:           The name of the tenant to which this user belongs
 :tenantId:         The integral, unique identifier of the tenant to which this user belongs
+:ucdn:              The name of the :abbr:`uCDN (Upstream Content Delivery Network)` to which the user belongs
+
+	.. versionadded:: 6.2
+
+	.. note:: This field is optional and only used if :abbr:`CDNi (Content Delivery Network Interconnect)` is in use.
+
 :uid:              A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX user ID of the user - now it is always ``null``
 :username:         The user's username
 
diff --git a/lib/go-rfc/http.go b/lib/go-rfc/http.go
index 08fa281..9590245 100644
--- a/lib/go-rfc/http.go
+++ b/lib/go-rfc/http.go
@@ -42,6 +42,7 @@ const (
 	Vary               = "Vary"                // RFC7231§7.1.4
 	Age                = "Age"                 // RFC7234§5.1
 	Location           = "Location"            // RFC7231§7.1.2
+	Authorization      = "Authorization"       // RFC7235§4.2
 )
 
 // These are (some) valid values for content encoding and MIME types, for
diff --git a/lib/go-tc/users.go b/lib/go-tc/users.go
index 2b2b953..ba1b0d1 100644
--- a/lib/go-tc/users.go
+++ b/lib/go-tc/users.go
@@ -294,6 +294,7 @@ type UserV40 struct {
 	Tenant               *string    `json:"tenant"`
 	TenantID             int        `json:"tenantId" db:"tenant_id"`
 	Token                *string    `json:"-" db:"token"`
+	UCDN                 string     `json:"ucdn"`
 	UID                  *int       `json:"uid"`
 	Username             string     `json:"username" db:"username"`
 }
diff --git a/traffic_ops/app/conf/cdn.conf b/traffic_ops/app/conf/cdn.conf
index 08fc3e3..3a349f8 100644
--- a/traffic_ops/app/conf/cdn.conf
+++ b/traffic_ops/app/conf/cdn.conf
@@ -100,7 +100,6 @@
         "state" : ""
     },
     "cdni" : {
-        "dcdn_id" : "",
-        "jwt_decoding_secret" : ""
+        "dcdn_id" : ""
     }
 }
diff --git a/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.down.sql b/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.down.sql
new file mode 100644
index 0000000..4d8916c
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.down.sql
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+ALTER TABLE tm_user DROP COLUMN IF EXISTS ucdn;
diff --git a/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.up.sql b/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.up.sql
new file mode 100644
index 0000000..e588817
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.up.sql
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+ALTER TABLE tm_user ADD COLUMN IF NOT EXISTS ucdn text NOT NULL DEFAULT '';
diff --git a/traffic_ops/traffic_ops_golang/api/api.go b/traffic_ops/traffic_ops_golang/api/api.go
index ff66ceb..0a7d3ce 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -48,6 +48,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault/backends/disabled"
 
+	"github.com/dgrijalva/jwt-go"
 	influx "github.com/influxdata/influxdb/client/v2"
 	"github.com/jmoiron/sqlx"
 	"github.com/lib/pq"
@@ -81,6 +82,11 @@ const (
 	TrafficVaultContextKey = "tv"
 )
 
+const (
+	MojoCookie  = "mojoCookie"
+	AccessToken = "access_token"
+)
+
 const influxServersQuery = `
 SELECT (host_name||'.'||domain_name) as fqdn,
        tcp_port,
@@ -1029,23 +1035,52 @@ func ParseDBError(ierr error) (error, error, int) {
 // GetUserFromReq returns the current user, any user error, any system error, and an error code to be returned if either error was not nil.
 // This also uses the given ResponseWriter to refresh the cookie, if it was valid.
 func GetUserFromReq(w http.ResponseWriter, r *http.Request, secret string) (auth.CurrentUser, error, error, int) {
-	cookie, err := r.Cookie(tocookie.Name)
-	if err != nil {
-		return auth.CurrentUser{}, errors.New("Unauthorized, please log in."), errors.New("error getting cookie: " + err.Error()), http.StatusUnauthorized
+	var cookie *http.Cookie
+
+	if r.Header.Get(rfc.Authorization) != "" && strings.Contains(r.Header.Get(rfc.Authorization), "Bearer") {
+		givenToken := r.Header.Get(rfc.Authorization)
+		tokenSplit := strings.Split(givenToken, " ")
+		if len(tokenSplit) > 1 {
+			givenToken = tokenSplit[1]
+		}
+		bearerCookie, err := getCookieFromAccessToken(givenToken, secret)
+		if err != nil {
+			return auth.CurrentUser{}, errors.New("unauthorized, please log in."), err, http.StatusUnauthorized
+		}
+		cookie = bearerCookie
+	} else {
+		for _, givenCookie := range r.Cookies() {
+			if cookie != nil {
+				break
+			}
+			if givenCookie == nil {
+				continue
+			}
+			switch givenCookie.Name {
+			case AccessToken:
+				bearerCookie, err := getCookieFromAccessToken(givenCookie.Value, secret)
+				if err != nil {
+					return auth.CurrentUser{}, errors.New("unauthorized, please log in."), err, http.StatusUnauthorized
+				}
+				cookie = bearerCookie
+			case tocookie.Name:
+				cookie = givenCookie
+			}
+		}
 	}
 
 	if cookie == nil {
-		return auth.CurrentUser{}, errors.New("Unauthorized, please log in."), nil, http.StatusUnauthorized
+		return auth.CurrentUser{}, errors.New("unauthorized, please log in."), nil, http.StatusUnauthorized
 	}
 
 	oldCookie, err := tocookie.Parse(secret, cookie.Value)
 	if err != nil {
-		return auth.CurrentUser{}, errors.New("Unauthorized, please log in."), errors.New("error parsing cookie: " + err.Error()), http.StatusUnauthorized
+		return auth.CurrentUser{}, errors.New("unauthorized, please log in."), errors.New("error parsing cookie: " + err.Error()), http.StatusUnauthorized
 	}
 
 	username := oldCookie.AuthData
 	if username == "" {
-		return auth.CurrentUser{}, errors.New("Unauthorized, please log in."), nil, http.StatusUnauthorized
+		return auth.CurrentUser{}, errors.New("unauthorized, please log in."), nil, http.StatusUnauthorized
 	}
 	db := (*sqlx.DB)(nil)
 	val := r.Context().Value(DBContextKey)
@@ -1075,6 +1110,38 @@ func GetUserFromReq(w http.ResponseWriter, r *http.Request, secret string) (auth
 	return user, nil, nil, http.StatusOK
 }
 
+func getCookieFromAccessToken(bearerToken string, secret string) (*http.Cookie, error) {
+	var cookie *http.Cookie
+	claims := jwt.MapClaims{}
+	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
+		return []byte(secret), nil
+	})
+	if err != nil {
+		return nil, fmt.Errorf("parsing claims: %w", err)
+	}
+	if token == nil {
+		return nil, errors.New("parsing claims: parsed nil token")
+	}
+	if !token.Valid {
+		return nil, errors.New("invalid token")
+	}
+
+	for key, val := range claims {
+		switch key {
+		case MojoCookie:
+			mojoVal, ok := val.(string)
+			if !ok {
+				return nil, errors.New("invalid token - " + MojoCookie + " must be a string")
+			}
+			cookie = &http.Cookie{
+				Value: mojoVal,
+			}
+		}
+	}
+
+	return cookie, nil
+}
+
 func AddUserToReq(r *http.Request, u auth.CurrentUser) {
 	ctx := r.Context()
 	ctx = context.WithValue(ctx, auth.CurrentUserKey, u)
diff --git a/traffic_ops/traffic_ops_golang/auth/authorize.go b/traffic_ops/traffic_ops_golang/auth/authorize.go
index 0801ed1..df230a1 100644
--- a/traffic_ops/traffic_ops_golang/auth/authorize.go
+++ b/traffic_ops/traffic_ops_golang/auth/authorize.go
@@ -44,6 +44,7 @@ type CurrentUser struct {
 	Role         int            `json:"role" db:"role"`
 	RoleName     string         `json:"roleName" db:"role_name"`
 	Capabilities pq.StringArray `json:"capabilities" db:"capabilities"`
+	UCDN         string         `json:"ucdn" db:"ucdn"`
 	perms        map[string]struct{}
 }
 
@@ -115,7 +116,8 @@ SELECT
   u.id,
   u.username,
   COALESCE(u.tenant_id, -1) AS tenant_id,
-  ARRAY(SELECT rc.cap_name FROM role_capability AS rc WHERE rc.role_id=r.id) AS capabilities
+  ARRAY(SELECT rc.cap_name FROM role_capability AS rc WHERE rc.role_id=r.id) AS capabilities,
+  u.ucdn
 FROM
   tm_user AS u
 JOIN
@@ -126,14 +128,14 @@ WHERE
 
 	var currentUserInfo CurrentUser
 	if DB == nil {
-		return CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "", []string{}, nil}, nil, errors.New("no db provided to GetCurrentUserFromDB"), http.StatusInternalServerError
+		return CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "", []string{}, "", nil}, nil, errors.New("no db provided to GetCurrentUserFromDB"), http.StatusInternalServerError
 	}
 	dbCtx, dbClose := context.WithTimeout(context.Background(), timeout)
 	defer dbClose()
 
 	err := DB.GetContext(dbCtx, &currentUserInfo, qry, user)
 	if err != nil {
-		invalidUser := CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "", []string{}, nil}
+		invalidUser := CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "", []string{}, "", nil}
 		if errors.Is(err, sql.ErrNoRows) {
 			return invalidUser, errors.New("user not found"), fmt.Errorf("checking user %v info: user not in database", user), http.StatusUnauthorized
 		}
@@ -160,7 +162,7 @@ func GetCurrentUser(ctx context.Context) (*CurrentUser, error) {
 			return nil, fmt.Errorf("CurrentUser found with bad type: %T", v)
 		}
 	}
-	return &CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "", []string{}, nil}, errors.New("No user found in Context")
+	return &CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "", []string{}, "", nil}, errors.New("No user found in Context")
 }
 
 func CheckLocalUserIsAllowed(form PasswordForm, db *sqlx.DB, ctx context.Context) (bool, error, error) {
@@ -181,6 +183,18 @@ func CheckLocalUserIsAllowed(form PasswordForm, db *sqlx.DB, ctx context.Context
 	return false, nil, nil
 }
 
+// GetUserUcdn returns the Upstream CDN to which the user belongs for CDNi operations.
+func GetUserUcdn(form PasswordForm, db *sqlx.DB, ctx context.Context) (string, error) {
+	var ucdn string
+
+	err := db.GetContext(ctx, &ucdn, "SELECT ucdn FROM tm_user where username=$1", form.Username)
+	if err != nil {
+		return "", err
+	}
+
+	return ucdn, nil
+}
+
 func CheckLocalUserPassword(form PasswordForm, db *sqlx.DB, ctx context.Context) (bool, error, error) {
 	var hashedPassword string
 
diff --git a/traffic_ops/traffic_ops_golang/cdni/shared.go b/traffic_ops/traffic_ops_golang/cdni/shared.go
index bcf3161..0775d6e 100644
--- a/traffic_ops/traffic_ops_golang/cdni/shared.go
+++ b/traffic_ops/traffic_ops_golang/cdni/shared.go
@@ -33,7 +33,6 @@ import (
 	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
-
 	"github.com/dgrijalva/jwt-go"
 	"github.com/lib/pq"
 )
@@ -73,14 +72,16 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+	if inf.Config.Cdni == nil || inf.Config.Secrets[0] == "" || inf.Config.Cdni.DCdnId == "" {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
 		return
 	}
 
-	ucdn, err := checkBearerToken(r.Header.Get("Authorization"), inf)
+	bearerToken := getBearerToken(r)
+
+	ucdn, err := checkBearerToken(bearerToken, inf)
 	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
 		return
 	}
 
@@ -106,6 +107,24 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 	api.WriteRespRaw(w, r, fciCaps)
 }
 
+func getBearerToken(r *http.Request) string {
+	if r.Header.Get(rfc.Authorization) != "" && strings.Contains(r.Header.Get(rfc.Authorization), "Bearer") {
+		givenTokenSplit := strings.Split(r.Header.Get(rfc.Authorization), " ")
+		if len(givenTokenSplit) < 2 {
+			return ""
+		}
+
+		return givenTokenSplit[1]
+	}
+	for _, cookie := range r.Cookies() {
+		switch cookie.Name {
+		case api.AccessToken:
+			return cookie.Value
+		}
+	}
+	return ""
+}
+
 func PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
 	if userErr != nil || sysErr != nil {
@@ -120,14 +139,15 @@ func PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+	if inf.Config.Cdni == nil || inf.Config.Secrets[0] == "" || inf.Config.Cdni.DCdnId == "" {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
 		return
 	}
 
-	ucdn, err := checkBearerToken(r.Header.Get("Authorization"), inf)
+	bearerToken := getBearerToken(r)
+	ucdn, err := checkBearerToken(bearerToken, inf)
 	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
 		return
 	}
 
@@ -190,14 +210,15 @@ func PutConfiguration(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+	if inf.Config.Cdni == nil || inf.Config.Secrets[0] == "" || inf.Config.Cdni.DCdnId == "" {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
 		return
 	}
 
-	ucdn, err := checkBearerToken(r.Header.Get("Authorization"), inf)
+	bearerToken := getBearerToken(r)
+	ucdn, err := checkBearerToken(bearerToken, inf)
 	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
 		return
 	}
 
@@ -472,12 +493,12 @@ func validateHostExists(host string, tx *sql.Tx) (int, error, error) {
 
 func checkBearerToken(bearerToken string, inf *api.APIInfo) (string, error) {
 	if bearerToken == "" {
-		return "", errors.New("bearer token header is required")
+		return "", errors.New("bearer token is required")
 	}
 
 	claims := jwt.MapClaims{}
 	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
-		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+		return []byte(inf.Config.Secrets[0]), nil
 	})
 	if err != nil {
 		return "", fmt.Errorf("parsing claims: %w", err)
@@ -517,8 +538,20 @@ func checkBearerToken(bearerToken string, inf *api.APIInfo) (string, error) {
 	if dcdn != inf.Config.Cdni.DCdnId {
 		return "", errors.New("invalid token - incorrect dcdn")
 	}
+
+	if ucdn != inf.User.UCDN {
+		return "", errors.New("user ucdn did not match token ucdn")
+	}
+
 	if ucdn == "" {
-		return "", errors.New("invalid token - empty ucdn field")
+		if inf.User.Can("ICDN:UCDN-OVERRIDE") {
+			ucdn = inf.Params["ucdn"]
+			if ucdn == "" {
+				return "", errors.New("admin level ucdn requests require a ucdn query parameter")
+			}
+		} else {
+			return "", errors.New("invalid token - empty ucdn field")
+		}
 	}
 
 	return ucdn, nil
diff --git a/traffic_ops/traffic_ops_golang/config/config.go b/traffic_ops/traffic_ops_golang/config/config.go
index 4062565..147d876 100644
--- a/traffic_ops/traffic_ops_golang/config/config.go
+++ b/traffic_ops/traffic_ops_golang/config/config.go
@@ -240,8 +240,7 @@ type ConfigInflux struct {
 }
 
 type CdniConf struct {
-	DCdnId            string `json:"dcdn_id"`
-	JwtDecodingSecret string `json:"jwt_decoding_secret"`
+	DCdnId string `json:"dcdn_id"`
 }
 
 // NewFakeConfig returns a fake Config struct with just enough data to view Routes.
diff --git a/traffic_ops/traffic_ops_golang/login/login.go b/traffic_ops/traffic_ops_golang/login/login.go
index 72661b4..2afcd45 100644
--- a/traffic_ops/traffic_ops_golang/login/login.go
+++ b/traffic_ops/traffic_ops_golang/login/login.go
@@ -42,9 +42,10 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tocookie"
 
+	jwt "github.com/dgrijalva/jwt-go"
 	"github.com/jmoiron/sqlx"
 	"github.com/lestrrat-go/jwx/jwk"
-	"github.com/lestrrat-go/jwx/jwt"
+	ljwt "github.com/lestrrat-go/jwx/jwt"
 )
 
 type emailFormatter struct {
@@ -156,6 +157,39 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
 				httpCookie := tocookie.GetCookie(form.Username, defaultCookieDuration, cfg.Secrets[0])
 				http.SetCookie(w, httpCookie)
 
+				var jwtToken *jwt.Token
+				var jwtSigned string
+				claims := jwt.MapClaims{}
+
+				emptyConf := config.CdniConf{}
+				if cfg.Cdni != nil && *cfg.Cdni != emptyConf {
+					ucdn, err := auth.GetUserUcdn(form, db, dbCtx)
+					if err != nil {
+						// log but do not error out since this is optional in the JWT for CDNi integration
+						log.Errorf("getting ucdn for user %s: %v", form.Username, err)
+					}
+					claims["iss"] = ucdn
+					claims["aud"] = cfg.Cdni.DCdnId
+				}
+
+				claims["exp"] = httpCookie.Expires.Unix()
+				claims[api.MojoCookie] = httpCookie.Value
+				jwtToken = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+
+				jwtSigned, err = jwtToken.SignedString([]byte(cfg.Secrets[0]))
+				if err != nil {
+					api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, err)
+					return
+				}
+
+				http.SetCookie(w, &http.Cookie{
+					Name:     api.AccessToken,
+					Value:    jwtSigned,
+					Path:     "/",
+					MaxAge:   httpCookie.MaxAge,
+					HttpOnly: true, // prevents the cookie being accessed by Javascript. DO NOT remove, security vulnerability
+				})
+
 				// If all's well until here, then update last authenticated time
 				tx, txErr := db.BeginTx(dbCtx, nil)
 				if txErr != nil {
@@ -370,15 +404,15 @@ func OauthLoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
 		if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
 			log.Warnf("Error parsing JSON response from oAuth: %s", err.Error())
 			encodedToken = buf.String()
-		} else if _, ok := result["access_token"]; !ok {
+		} else if _, ok := result[api.AccessToken]; !ok {
 			sysErr := fmt.Errorf("Missing access token in response: %s\n", buf.String())
 			usrErr := errors.New("Bad response from OAuth2.0 provider")
 			api.HandleErr(w, r, nil, http.StatusBadGateway, usrErr, sysErr)
 			return
 		} else {
-			switch t := result["access_token"].(type) {
+			switch t := result[api.AccessToken].(type) {
 			case string:
-				encodedToken = result["access_token"].(string)
+				encodedToken = result[api.AccessToken].(string)
 			default:
 				sysErr := fmt.Errorf("Incorrect type of access_token! Expected 'string', got '%v'\n", t)
 				usrErr := errors.New("Bad response from OAuth2.0 provider")
@@ -392,10 +426,10 @@ func OauthLoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
 			return
 		}
 
-		decodedToken, err := jwt.Parse(
+		decodedToken, err := ljwt.Parse(
 			[]byte(encodedToken),
-			jwt.WithVerifyAuto(true),
-			jwt.WithJWKSetFetcher(jwksFetcher),
+			ljwt.WithVerifyAuto(true),
+			ljwt.WithJWKSetFetcher(jwksFetcher),
 		)
 		if err != nil {
 			api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, errors.New("Error decoding token with message: "+err.Error()))
diff --git a/traffic_ops/traffic_ops_golang/login/logout.go b/traffic_ops/traffic_ops_golang/login/logout.go
index 955d153..348c811 100644
--- a/traffic_ops/traffic_ops_golang/login/logout.go
+++ b/traffic_ops/traffic_ops_golang/login/logout.go
@@ -23,6 +23,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
@@ -42,6 +43,14 @@ func LogoutHandler(secret string) http.HandlerFunc {
 
 		cookie := tocookie.GetCookie(inf.User.UserName, 0, secret)
 		http.SetCookie(w, cookie)
+		http.SetCookie(w, &http.Cookie{
+			Name:     "access_token",
+			Value:    "",
+			Path:     "/",
+			Expires:  time.Now().Add(0),
+			MaxAge:   0,
+			HttpOnly: true, // prevents the cookie being accessed by Javascript. DO NOT remove, security vulnerability
+		})
 		resp := struct {
 			tc.Alerts
 		}{tc.CreateAlerts(tc.SuccessLevel, "You are logged out.")}
diff --git a/traffic_ops/traffic_ops_golang/routing/middleware/wrappers_test.go b/traffic_ops/traffic_ops_golang/routing/middleware/wrappers_test.go
index ee677e3..29303e8 100644
--- a/traffic_ops/traffic_ops_golang/routing/middleware/wrappers_test.go
+++ b/traffic_ops/traffic_ops_golang/routing/middleware/wrappers_test.go
@@ -234,7 +234,7 @@ func TestWrapAuth(t *testing.T) {
 
 	f(w, r)
 
-	expectedError := `{"alerts":[{"text":"Unauthorized, please log in.","level":"error"}]}` + "\n"
+	expectedError := `{"alerts":[{"text":"unauthorized, please log in.","level":"error"}]}` + "\n"
 
 	if *debugLogging {
 		fmt.Printf("received: %s\n expected: %s\n", w.Body.Bytes(), expectedError)
diff --git a/traffic_ops/traffic_ops_golang/user/user.go b/traffic_ops/traffic_ops_golang/user/user.go
index 746c48e..8cd9df7 100644
--- a/traffic_ops/traffic_ops_golang/user/user.go
+++ b/traffic_ops/traffic_ops_golang/user/user.go
@@ -527,7 +527,8 @@ func UpdateQueryV40() string {
 	postal_code=:postal_code,
 	country=:country,
 	tenant_id=:tenant_id,
-	local_passwd=COALESCE(:local_passwd, local_passwd)
+	local_passwd=COALESCE(:local_passwd, local_passwd),
+	ucdn=:ucdn
 	WHERE id=:id
 	RETURNING last_updated,
 	 (SELECT t.name FROM tenant t WHERE id = u.tenant_id),
@@ -552,7 +553,8 @@ func InsertQueryV40() string {
 	postal_code,
 	country,
 	tenant_id,
-	local_passwd
+	local_passwd,
+	ucdn
 	) VALUES (
 	:username,
 	:public_ssh_key,
@@ -569,7 +571,8 @@ func InsertQueryV40() string {
 	:postal_code,
 	:country,
 	:tenant_id,
-	:local_passwd
+	:local_passwd,
+	:ucdn
 	) RETURNING id, last_updated,
 	(SELECT t.name FROM tenant t WHERE id = tm_user.tenant_id),
 	(SELECT r.name FROM role r WHERE id = tm_user.role)`
@@ -598,7 +601,8 @@ SELECT
 	u.registration_sent,
 	u.tenant_id,
 	t.name AS tenant,
-	u.last_updated,`
+	u.last_updated,
+	u.ucdn,`
 
 const readQuery = readBaseQuery + `
 u.last_authenticated,
@@ -607,7 +611,7 @@ r.name as role
 FROM tm_user u
 LEFT JOIN tenant t ON u.tenant_id = t.id
 LEFT JOIN role r ON u.role = r.id
-LEFT JOIN role_capability rc on rc.role_id = r.id
+LEFT JOIN role_capability rc ON rc.role_id = r.id
 `
 
 const legacyReadQuery = readBaseQuery + `
diff --git a/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html b/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
index dd98f71..b1d4c1c 100644
--- a/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
@@ -82,6 +82,14 @@ under the License.
                     <small ng-show="user.tenantId"><a href="/#!/tenants/{{user.tenantId}}" target="_blank">View Details&nbsp;&nbsp;<i class="fa fs-xs fa-external-link"></i></a></small>
                 </div>
             </div>
+            <div class="form-group" ng-class="{'has-error': hasError(userForm.uCDN), 'has-feedback': hasError(userForm.uCDN)}">
+                <label for="uCDN" class="control-label col-md-2 col-sm-2 col-xs-12">Upstream CDN</label>
+                <div class="col-md-10 col-sm-10 col-xs-12">
+                    <input id="uCDN" name="uCDN" type="text" class="form-control" ng-model="user.ucdn" ng-pattern="/^\S*$/">
+                    <small class="input-error" ng-show="hasPropertyError(userForm.ucdn, 'pattern')">No Spaces</small>
+                    <span ng-show="hasError(userForm.ucdn)" class="form-control-feedback"><i class="fa fa-times"></i></span>
+                </div>
+            </div>
             <div class="form-group" ng-class="{'has-error': hasError(userForm.uPass), 'has-feedback': hasError(userForm.uPass)}">
                 <label class="control-label col-md-2 col-sm-2 col-xs-12">Password <span ng-if="settings.isNew">*</span></label>
                 <div class="col-md-10 col-sm-10 col-xs-12">
diff --git a/traffic_portal/test/integration/Data/users.ts b/traffic_portal/test/integration/Data/users.ts
index 884b4b3..92bb871 100644
--- a/traffic_portal/test/integration/Data/users.ts
+++ b/traffic_portal/test/integration/Data/users.ts
@@ -53,6 +53,7 @@ export const users = {
                     Email: "test@cdn.tc.com",
                     Role: "admin",
                     Tenant: "- tenantSame",
+                    UCDN: "",
                     Password: "qwe@123#rty",
                     ConfirmPassword: "qwe@123#rty",
                     PublicSSHKey: "",
diff --git a/traffic_portal/test/integration/PageObjects/UsersPage.po.ts b/traffic_portal/test/integration/PageObjects/UsersPage.po.ts
index 00b7e26..6d52288 100644
--- a/traffic_portal/test/integration/PageObjects/UsersPage.po.ts
+++ b/traffic_portal/test/integration/PageObjects/UsersPage.po.ts
@@ -27,6 +27,7 @@ interface User {
   Email: string;
   Role: string;
   Tenant: string;
+  UCDN: string;
   Password: string;
   ConfirmPassword: string;
   PublicSSHKey: string;
@@ -63,6 +64,7 @@ export class UsersPage extends BasePage {
     private txtEmail = element(by.name('email'));
     private txtRole = element(by.name('role'));
     private txtTenant = element(by.name('tenantId'));
+    private txtUCDN = element(by.name('uCDN'));
     private txtPassword = element(by.name('uPass'));
     private txtConfirmPassword = element(by.name('confirmPassword'));
     private txtPublicSSHKey = element(by.name('publicSshKey'));
@@ -110,6 +112,7 @@ export class UsersPage extends BasePage {
       await this.txtEmail.sendKeys(this.randomize + user.Email);
       await this.txtRole.sendKeys(user.Role);
       await this.txtTenant.sendKeys(user.Tenant+this.randomize);
+      await this.txtUCDN.sendKeys(user.UCDN);
       await this.txtPassword.sendKeys(user.Password);
       await this.txtConfirmPassword.sendKeys(user.ConfirmPassword);
       await this.txtPublicSSHKey.sendKeys(user.PublicSSHKey);