deft/notes/cisco_ft_securex_registration.org
2022-03-03 16:19:48 +01:00

23 KiB

Cisco FT SecureX Simplified Registration

tags
Auth
ref
https://github.com/advthreat/response/issues/821
source
https://github.com/advthreat/iroh/issues/6076
dashboard
https://github.com/advthreat/iroh/projects/32

17 Technical Plan

Estimate: 17 rcd

rcd
rcd stands for release cycle for 1 dev

DONE 0 Support private email vs public emails

DONE Estimate: 1 rcd after the list is provided.

The solution is to use a blacklist of domains where any user could create multiple email accounts pseudo-anonymously.

Details: https://github.com/advthreat/response/issues/979

CANCELED Support allow-list exceptions for some Cisco user.

  • State "CANCELED" from "HOLD" [2022-01-17 Mon 10:57]
    This only concern new account creation. User with gmail account could still login.

Estimate: 1 rcd after the list is provided.

Typically we should allow some users with an email like some-user+XXX@gmail.com

1 Support, search admin with same email domain

Estimate: 1 rcd. Note: Work in progress

We should be able given an email from a user, to find all the orgs for which at least one of its admin has a matching domain name.

  1. Most efficient: add an invisible field email-domain to all users. This should be lower-case, and we will need a migration. Doing this we could have a faster match than using string related queries.

Problems, users can login in the same user, with the same public email with different emails. This should be rare.

  1. Search via text match.

The algorithm should look a bit like:

;; only when this is an unknown user
;; so a single approval will prevent the user to see this page.
(let [user-email ,,,
      domain (string/replace user-email #".*@" "")
      users (matching-admins domain) ;; returns a potentially big list of admin users
      indexed-orgs (group-by :org-id users)]
  (vals indexed-orgs))

Once this list of orgs is found. We should also check the list of pending or rejected OrgAccessRequest for this user in order to prevent the user to request access multiple time.

12 New UI Sync

Estimate: 12 rcd

We should find a way to hand the UI work to the UI team. Right now, the page are all generated in IROH.

To reach that ideally we should sync the source code as a jar in IROH.

In order to give the UI the ability to make a front-end application, we should create news APIS that support a new UserIdentity-level JWT

1 New Auth APIs

Estimate: 1 rcd

ref :: https://github.com/advthreat/iroh/issues/6242

Continue to use the /code route of iroh-auth. But instead of returning a Session Token, it will return a UserIdentity Token. This is a JWT with the important data from the IdP:

  • idp-id
  • user-identity-id
  • user-email
  • etc…

When the user is redirected to an HTML generated page, we should add the code in the anchor parameter of the route so the UI will be able to use that code to retrieve a UserIdentity Token. We should derive the current mechanism with code, but use the login code store.

Tasks
  • Create a new function to generate these new JWT using the "token-infos"

1 New API JWT middleware

Estimate: 1 rcd

ref
https://github.com/advthreat/iroh/issues/6266

We need to change the configuration of the check JWT middleware to support UserIdentity Token instead. And use this configuration for this new API.

The user-identity-jwt should contain enough data to retrieve:

  • idp-mapping
  • user-email
  • all other metas, as user-name, user-nick, etc…

5 New IROH-Auth API endpoints

Estimate: 5 rcd

ref
https://github.com/advthreat/iroh/issues/6268
4 OrgAccessRequestService

Estimate: 4 rcd

ref
https://github.com/advthreat/iroh/issues/6272

This new service should be mostly a CRUD service on top of TKStore with the following schema:

(s/defschema OrgAccessRequestStatus
  (s/enum :pending :accepted :rejected))

(s/defschema OrgAccessRequest
  (st/merge
   {:id UUID
    :secret s/Str ;; should be generated via `iroh-auth.lib.security/password`
    :idp-mapping IdPMapping
    :user-email s/Str
    :org-id s/Str
    :status OrgAccessRequestStatus
    :created-at DateTime}
   (st/optional-keys
    {:user-name s/Str
     :user-nick s/Str
     :granted-role Role ;; the role granted if the request is accepted
     :approver-id UserId
     :approver-email UserEmail ;; email of the approver
     :updated-at DateTime
     })))
(defprotocol OrgAccessRequestService
  "See iroh-auth.registration.org-access-request.schemas/ServiceFns for schemas."
  :extend-via-metadata true

  ;; service function for the Admins logged in SecureX
  ;; User filtered CRUD+Search for REST API related methods
  (search-org-access-requests-for-org
    [this request-identity filter-map pagination-params]
    "Search all OrgAccessRequest of the org of the user of the request-identity")

  (get-org-access-request
    [this request-identity org-access-request-id]
    "Return the OrgAccessRequest for a user using the org-access-request-id")

  (patch-org-access-request
    [this request-identity org-access-request-id org-access-request-patch]
    "Change the status of an OrgAccessRequest to grant/reject it.
     Note user creation could be a side effect.")

  ;; For the New Registration Page (the user logged in via the IdP successfully)
  (search-org-access-requests-for-user-identity
    [this user-identity filter-map pagination-params]
    "Search all OrgAccessRequest made by this user identity accross all orgs.
     This should only be visible via the registration page on the new API accepting
     user-identity JWT")

  (create-org-access-request
    [this user-identity org-id]
    "Create a new OrgAccessRequest.")

  (delete-org-access-request
    [this user-identity org-access-request-id]
    "Remove an org request access.")

  ;; Internal CRUD+Search
  (raw-search-org-access-requests
    [this filter-map pagination-params]
    "Search all OrgAccessRequest grants")

  (raw-get-org-access-request
    [this org-access-request-id]
    "Return the OrgAccessRequest grant")

  (raw-patch-org-access-request
    [this org-access-request-id org-access-request-patch]
    "Update the status of an OrgAccessRequest.")

  ;; To be used in the `iroh-auth-web-service` directly without any password
  (patch-org-access-request
    [this org-access-request-id org-access-request-secret org-access-request-patch]
    "Similar to patch-org-access-request but for non logged in users can be used by
     providing both the OrgAccessRequest id and secret.
     That way we could build a URL into emails sent to admins to create
     the new user in the correct org."))
1 search/get/patch

Estimate: 1 rcd

1 Create

For the create-org-access-request an email should be send to the oldest active admin of the matching org.

3 PATCH

Estimate: 3 rcd

The org-access-request-patch should have the following schema:

{(s/optional-key :role) Role
 :status (s/enum :accepted :rejected)}

If the role is not provided, the default value should be user. If the old OrgAccessRequest is not :pending this should throw a 400. If the status is set to :accepted:

  1. create a new user for the org of the OrgAccessRequest.
  2. send an email to the user that requested the access

If the status is set to :rejected, then send an email to the user that requested the access.

Note we should test that the user created that way could login correctly.

1 Unauthenticated PATCH

Estimate: 1 rcd

Similar to the PATCH but should check the authenticity of the call by checking both the OrgAccessRequest id and secret.

1 OPTIONAL UserIdentityService

Estimate: 1 rcd

ref
https://github.com/advthreat/iroh/issues/6274

Create a new service dedicated to UserIdentities.

(defprotocol UserIdentityService
  "Service which handle UserIdentities (mostly CRUD operations). "
  (create-user-identity [this user-params])
  (get-user-identity [this user-id])
  (update-user-identity [this user-params])
  (update-login-date [this user-id])
  (search-user-identities
    [this filter-map pagination-params]
    [this filter-map pagination-params stringmatch-query-params])
  (delete-user-identity [this user-id]))

It should also contain a field containing a set of hidden org-ids that will be the orgs not to display on the registration webpages.

(s/defschema UserIdentity
  (ist/open-schema-any-keys
   {:id s/Str
    :email
    :name
    :nickname
    :last-logged-in [,,,]
    (s/optional-key :hidden-orgs) #{,,,}}))

4 New IROH-Auth API

Estimate: 4 rcd

ref
https://github.com/advthreat/iroh/issues/6268

Depends: IROH-Auth CRUD Service

This new API should only work with UserIdentity JWT.

GET  /iroh/iroh-auth-ui/whoami ;; whoami at the User Identity level
POST /iroh/iroh-auth-ui/org-access/:org-id ;; request access to a matching org
POST /iroh/iroh-auth-ui/hide-org/:org-id ;; ability to hide an org-access
GET  /iroh/iroh-auth-ui/matching-accounts ;; list all the matching accounts
GET  /iroh/iroh-auth-ui/matching-orgs ;; list all the matching orgs
GET  /iroh/iroh-auth-ui/pending-invites ;; list all the pending invites
GET  /iroh/iroh-auth-ui/registration-view ;; helper to make a single http call
whoami Endpoint
ref
https://github.com/advthreat/iroh/issues/6266

Write an endpoint that would use the UserIdentity JWT and decode it. Ideally should instead read the value in DB via the UserIdentityService.

Create New Account

Estimate: 2 rcd

ref
https://github.com/advthreat/iroh/issues/6280

Create a new endpoint to create a new account (user + org):

GET  /iroh/iroh-auth-ui/create-new-account

Accept: application/json
Content-Type: application/json
User-Agent: ob-http
Authorization: Bearer ${user-identity-jwt}

{"org-name":"Cisco",
 "country":"US"}

The clojure code for the route should roughly look like:

(s/defschema NewAccount
  (st/merge
   {:org-name :- s/Str
    :country :- (apply s/enum country-iso-codes)
    (st/optional-keys
     {:department :- s/Str
      :street1 :- s/Str
      :street2 :- s/Str
      :postal-code :- s/Str
      :city :- s/Str})}))

(POST "/create-new-account" []
  :summary "Given a code and some org-settings create a new account (new org and new user)"
  :description "This is an internal temporary route needed to select the user/org."
  :body  [{:keys [country
                  org-name
                  department
                  street1
                  street2
                  postal-code
                  city] :as  new-account} NewAccount]
  (let [address (iroh-core/assoc-some?
                 {:country-iso-code country}
                 :department department
                 :street1 street1
                 :street2 street2
                 :postal-code postal-code
                 :city city)
        org-settings {:name org-name
                      :address address}]
    (resp/found (create-new-account org-settings))))

As we now have a session, we should take care about a few details:

Important Security Remarks
Should we keep track of the origin?

When a user login there is always an origin parameter (by default it will point to SecureX). But a user can login and create an account and should ultimately be redirected to the correct URL (product). Currently, the user can be redirected to:

  • SecureX
  • CTR
  • Orbital

We should put the origin in the JWT and we should take care that the origin has been checked via the allowed-login-origins.

The question about should we return a 301 to the create-new-account is open. But this is probably a good idea to let only the backend take care of the security of the redirection. Note this is a very important security concern otherwise an attack in the redirection code might send the credentials to an attacker quite easily.

Should we prevent a user identity to create multiple accounts?

I don't think so. Not in the first round at least. It will probably be easy to add a created-by metas in the org, and prevent duplicates (or put a maximal number of authorized enabled orgs)

List Matching Accounts - ref ::

https://github.com/advthreat/iroh/issues/6270 #+begin_src http GET /iroh/iroh-auth-ui/matching-accounts

Accept: application/json Content-Type: application/json User-Agent: ob-http Authorization: Bearer ${user-identity-jwt}

#+end_src

It should return a list of object with the following schema sorted by last logged in time by the user:

{:user User
 :org Org
 :last-login-date DateTime
 :relative-last-login s/Str
 :org-created-at DateTime
 :relative-org-created-at s/Str
 :activated? s/Bool
 :org-nb-users s/Int}

Mainly should return the data structure used in the current selection account page using similar order and functionalities.

List Pending Invites
ref
https://github.com/advthreat/iroh/issues/6269
GET  /iroh/iroh-auth-ui/pending-invites

Here is an example value:

[{:role "admin",
  :org-id "org-1",
  :expires-in-nb-days 7,
  :status "pending",
  :invitee-email "chuck@example.org",
  :inviter-user-id "org-1-admin-1"}]

We already retrieve pending invite in the code. The work would be about taking care of having a specialized method from either an existing or a new TK service dedicated to this functionality.

Use this method to expose it via the API.

Request Org Access
ref
https://github.com/advthreat/iroh/issues/6271

We need to create another store with another Entity for access request to an Org.

(s/defschema OrgAccessRequest
  (st/merge
   {:id UUID
    :idp-mapping IdPMapping
    :user-email s/Str
    :org-id s/Str
    :status (s/enum :pending :accepted :rejected)
    :created-at DateTime}
   (st/optional-keys
    {:user-name s/Str
     :user-nick s/Str
     :approver-id UserId
     :approver-email UserEmail ;; email of the approver
     :updated-at DateTime
     })))

When a user request access to an organization. We should create this object in DB.

POST  /iroh/iroh-auth-ui/org-access/:org-id ;; request access to a matching org
POST /iroh/iroh-auth-ui/org-access/:org-id

Accept: application/json
Content-Type: application/json
User-Agent: ob-http
Authorization: Bearer ${user-identity-jwt}

With this, and if every check matches:

  1. There is a known active admin of this org with an email with the same domain name
  2. The org is active

We should:

  1. create a new OrgAccessRequest object in DB.
  2. Send emails (see the Email Notification of Org Request Accesses section)
List Matching Orgs
ref
https://github.com/advthreat/iroh/issues/6273
GET  /iroh/iroh-auth-ui/matching-orgs

Given a UserIdentity with some email. Should return every Org whose at least one admin has an email with the same domain address. So if you login via chuck@cisco.com you should return all orgs for which there is at least one admin with a cisco.com email address.

Should return a list of objects with the following schema (sort to be defined):

{:org Org
 :org-nb-users s/Int
 :org-request-access-status OrgRequestAccessStatus}

Note

This endpoint is not straightforward. We should list all current matching org, and concurrently list all current OrgRequestAccess made by this UserIdentity. We should also list all orgs for which there is already a pending invite.

We shall finally return a list of orgs sorted by the number of users (from max to min) with the current status and remove the org for which there is still a non expired pending invite.

If there is more than 6 orgs, we should only return the firsts 6 of them but make it clear in the pagination headers that there are more than 6 matching org so the frontend could react accordingly. Another option would be to return a 400 with an error message.

OPTIONAL Hide some matching orgs manually

Optionally we might also retrieve the orgs marked as hidden for this UserIdentity in DB.

We should support query parameter to show all orgs, even the hidden ones so a user should be able to unhide a previously hidden org.

Registration View
ref
https://github.com/advthreat/iroh/issues/6275
GET  /iroh/iroh-auth-ui/registration-view

Should return the same result as the union of the calls to matching-accounts, matching-orgs and pending-invites.

{:matching-accounts [,,,]
 :matching-orgs [,,,]
 :pending-invites [,,,]}
OPTIONAL Hide Matching Org
ref
https://github.com/advthreat/iroh/issues/6276
POST /iroh/iroh-auth-ui/hide-org/:org-id

Accept: application/json
Content-Type: application/json
User-Agent: ob-http
Authorization: Bearer ${user-identity-jwt}

Should save in the UserIdentity store a new org-id to hide. This impacts the call to matching-orgs such that orgs marked as hidden should not be displayed when calling the matching-org route.

Redirects User

Create a new endpoint to redirect users using the value in the UserIdentity JWT. See https://github.com/advthreat/iroh/issues/6280

GET /iroh/iroh-auth-ui/redirect

So this should be a route which should looks like this:

(GET "/redirect" []
  :summary "Given a code and some org-settings create a new account (new org and new user)"
  :description "This is an internal temporary route needed to select the user/org."
  :identity [session-redirect]
  (if (allowed-login-origin? session-redirect)
    (resp/found session-redirect)
    (err/bad-request :forbidden-redirect-url
                     (format "The redirect URL %s is not a valid redirect for login."
                             session-redirect)
                     {})))

1 OPTIONAL Sync UserIdentity on all successful SecureX login

Estimate: 1 rcd

ref
https://github.com/advthreat/iroh/issues/6367

From #6076 we have a new CRUD service that should keep track of `UserIdentity`. Mainly this information could be used as providing more details than what is currently set int he user's `idp-mapping`. Typically we have access to the email of the UserIdentity, user name from the IdP, and probably a lot more details that will be useful.

So every time a successful login occurs we should update the UserIdentity object last logged in date (as we do for users) which will probably be quite useful.

3 Email Notification of Org Request Accesses

Estimate: 3 rcd after email+html templates

ref
https://github.com/advthreat/iroh/issues/6368
?
List all the admins of the requested org.

2.a. If there is fewer admins than a number that could be configured in the node configuration. Then we send an email to all admins. 2.b. If there are more admins than this specific number, then we randomly chose this maximal number of admins and send them an email notification.

TODO Have an email template (both HTML and Text)

Notes

Need to be able to trigger a new request to join after 7 days.

2 Org Requests CRUD API for Admins of the Orgs

Estimate: 2 rcd after mail templates + text

ref
https://github.com/advthreat/iroh/issues/6369

There should be a CRUD API restricted to the admin/user-mgmt/org-requests scope:

  • GET /iroh/user-mgmt/org-requests list pending org access requests
  • GET /iroh/user-mgmt/org-requests/<id> read a single org access request
  • PATCH /iroh/user-mgmt/org-requests/<id> Grant or Reject the access

List

GET /iroh/user-mgmt/org-requests

If no parameter is provided, only list pending OrgAccessRequests of the org of the caller. Otherwise we could pass the query-parameter status with the following value(s):

  • pending
  • accepted
  • rejected

Note we should probably support duplicate statuses. Ex:

GET /iroh/user-mgmt/org-requests?status=accepted&status=pending

Read

GET /iroh/user-mgmt/org-requests/org-request-id

Should returns a 404 if not found or the single Org Access Request object.

Patch the Org Access

PATCH /iroh/user-mgmt/org-requests/<id> Grant/Reject the access

The body should be a JSON Object with:

  • an optional field role whose value could be either admin or user (default to user)
  • a mandatory status field whose value could be either accepted or rejected.

A few examples of valid values:

{"granted-role":"admin"
 "status":"accepted"}
{"status":"accepted"}
{"status":"rejected"}

During the call we should:

  1. Create a new user with:
{:user-id (gen-uuid)
 :org-id (:org-id org-access-request)
 :user-email (:user-email org-access-request)
 :idp-mappings [(:idp-mapping org-access-request)]
 :user-name (:user-name org-access-request)
 :user-nick (:user-nick org-access-request)
 :role (get-in request [:body :role])
 :enabled? true
 }
  1. Send an email to user confirming his access was granted.

TODO have an email template + text.