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 <[email protected]>
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: "[email protected]",
                     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);

Reply via email to