ЕСОЗ - публічна документація

[deprecated] 2-factor authentication - draft 02

Requirements

High-level tasks:

  • mithril model and API changes - ave phone number, get services. Changes in concept user, party employee, party_users.
  • Change Create employee process - add 2nd factor phone number, add phone number verification step
  • Change approve employee process - two-step approval, resend sms
  • Change login process - step, resend sms, otp counter, block user after n logins failed
  • Existing users migration - move the mobile phone number to the 2nd factor
  • Mithril admin changes - show 2nd factor, block/unblock user
  • Introduce user endpoints to MIS

Assumtions:

  • We keep using oauth 2.0 flow.
  • 2factor auth will be introduced for all users at the same time
  • mobile phone number for all the existing users should be automatically used as the 2nd factor
  • 2nd factor can be changed only via create employee request
  • 2nd factor - OTP delivered via sms
  • 2-factor auth can be applied to all the admin consoles: mithrill, uaddresses, nhs.admin - configurable

Key aspects of implementation

  • We implemented only 2FA (but MANY FA)
  • User must authorize for 2FA (second step) only after successful login with emal+password (first step)
  • We support a few kind of factors, but user have only 1 active factor at the same time 
  • Now we implement only `SMS` factor type 
  • We add blocking process (in case OTP error) for verifying step 
  • We create additional blocking process (in case error password) for normal login process (login + password)
  • Logic 2FA process based on same token_types (access_token_type && 2fa_access_token_type)
  • 2FA data store at separate entity `mithril.authentication_factors` (without status model, but with logical state for 2fa & user item)
  • OTP data store at existing separate entity `mithril.otp` (with status model)
  • At same time we have 1 active OTP for 1 factor

Configs

2FA & OTP configurable parameters:

ParamsTypePurpose
OTP_ERROR_MAXinteger

Maximum for entered in-error trying for OTP verification process for OTP item.  After excess of quantity entered in-error trying, status for OTP automatically sets to BLOCKED.

OTP_LIFETIMEinteger Life-time for OTP item
OTP_LENGTHintegerLength of OTP value

USER_LOGIN_ERROR_MAX

integerMaximum for entered in-error trying for login verification process for all user life-time.  After excess of quantity entered in-error trying, status for user automatically sets to BLOCKED.
USER_OTP_ERROR_MAXintegerMaximum for entered in-error trying for OTP verification process for all user life-time.  After excess of quantity entered in-error trying, status for user automatically sets to BLOCKED.
USER_2FA_ENABLEDbooleanFlag which determinate is a default activating 2FA for new user.


Data structure

Store 2FA structure for User

  • According ability uses many factors of authentication - propose use separated new entity `authentication_factors` with a structure:

    ColumnTypePurpose
    iduuid
    user_iduuidUser FK 
    typevarchar
    factorvarcharFactor value
    is_activeboolean
    inserted_attimestamp
    updateed_attimestamp

    additional constraint: create unique index (user_id, type)

    where factor is string which depends on factor type, examples:

    typefactor 

    SMS

    "+380677778899"
    PHONE"+380677778899"
    EMAIL"pupkin.vasya@gmail.com"

Change User entity

  • We need store for user 3 attribute:
    • count logins fail
    • count 2FA fail
    • block status of user
  • Propose store counters parameters for 2FA in `users.priv_settings` as structure :

    {
    	"login_error_counter": 0,
    	"otp_error_counter": 0
    }
  • Propose store information about blocked directly in entity `users`.
    New columns:

    ColumnTypePurpose
    is_blockedboolean
    block_reasonvarchar (255), NULL

Сonstraints

  • user have one active 2FA factor ( one of any type)
  • unique index for `authentication_factors` on user_id+type

2FA state chart

We need minimal 4 state for user & 2FA:

  • DISABLED - Disable 2FA for this user (via NHS Admin)
  • BLOCKED - User blocked after N-unsuccessful attempts via authentication process.
  • RESET - NHS Admin reset value of factor or factor type. At first login user must fill value of factor.
  • ACTIVE - normal status: 2FA enable 

Propose don't use statuses model, and use logic calculating for state 2FA user:

 

Logic status for 2FA & user Purpose
RESET

User don't blocked (`is_blocked`=false) & exist 1 active item in `authentication_factors`  with empty value of factor .
Examples for type = SMS:

(      factor = NULL  or factor = "" )

ACTIVEUser don't blocked (`is_blocked`=false) & exist 1 active item in `authentication_factors`  with fill value of factor. 

Examples for type = SMS:

(       factor= "+380677778899"  )

DISABLED

User don't blocked (`is_blocked`=false) & not exist any active items in `authentication_factors` 
(SELECT * FROM authentication_factors AS 2FA 
WHERE 2FA.user_id = $.user_id AND 2FA.is_active = 1

-- return  0 rows)

BLOCKED

User have `is_blocked`=true 

(SELECT * FROM users AS U
WHERE U.id = $.user_id AND U.is_blocked = true

--- return 1 rows)


OTP Module

Data structure 

Entity `OTP` with a structure:

ColumnTypePurpose
iduuid
keyvarcharValue of factor (1 active 2FA for user) 
codevarcharValue of OTP
statusvarcharStatus (Dictionary: OTP_STATUS)
code_expired_attimestampTimestamp which OTP expired (now() + param from config)
attempts_countintegerCount of trying OTP authorization process
updateed_attimestamp

OTP states 

  • Dictionary: `OTP_STATUS`

    ValuePurpose
    NEWInitial status for newest OTP item. Ready to use in OTP verification process.
    VERIFIEDStatus for successful OTP verification process
    UNVERIFIEDStatus for unsuccessful OTP verification process
    EXPIREDExpired after OTP lifetime.
    CANCELED

    Status for manual admin action.

  • Status Chart for OTP
  • Transitions
    From
    Transition
    Result

    Created new OTPstatus = NEW
    status = NEWSuccseful OTP verifystatus = VERIFIED
    status = NEWUnsuccseful OTP verify afterexcess of count [param: OTP_ERROR_MAX] status = UNVERIFIED
    status = NEW[AUTO] Termination process after end of life-time OTP [param: OTP_LIFETIME]status = EXPIRED
    status = NEWAll OTP by `key` in status=`NEW` before creating new OTPstatus = CANCELED

Services specifications

1- WS.Create User (Modification) DONE

Purpose

  • Add `is_blocked`, `block_reason` column in user entity
  • Add logic for create 2FA item for new user 

Request parameters 

Add new parameters:

  • 2fa_enable (boolean, optional

Logic WS

  1. Create user with this structure in `users.priv_settings`:

    {
        "login_error_counter": 0,
        "otp_error_counter": 0
    }

    and:

    1. is_blocked = FALSE
    2. block_reason = NULL
  2. .....
  3. After insert new user in `users`
  4. Analyze $.2fa_enable 
    1. If $.2fa_enable = FALSE - break 
    2. If $.2fa_enable = TRUE 
      1. Insert new record in `authentication_factors` with logical status = RESET
        1. user_id = $.user_id
        2. type = `SMS`
        3. factor = NULL
        4. is_active = TRUE
        5. update_at = now()
    3. If $.2fa_enable = NULL or empty
      1. Get config-param `USER_2FA_ENABLED`
      2. If `USER_2FA_ENABLED` = TRUE
        1. Insert new record in `authentication_factors` with logical status = RESET
          1. user_id = $.user_id
          2. type = `SMS`
          3. factor = NULL
          4. is_active = TRUE
          5. update_at = now()

2- WS: UpdateUser2FA (new EP) DONE

APIary

PUT mithril/api/users/{user_id}/2fa/{2fa_id} 

Scope: `2fa:write`

Purpose

Disable/Enable 2FA for user (via Admin-console).

Request parameters

  • user_id
  • 2fa_id
  • is_active

Logic WS

  • Validate token & scope
  • Validate user_id FK
  • Validate 2fa_id FK
  • Get 2FA item by 2fa_id  for non-blocked user by $.user_id

    SELECT *
    FROM authentication_factors AS 2FA
    	INNER JOIN user AS U
    		ON 2FA.user_id = U.id
    WHERE 
    	U.id = $.user_id
    		AND 2FA.id = $.2fa_id
    		AND U.is_active = TRUE
    		AND U.is_blocked = FALSE
    • If  exist 2FA item for user →  update 2FA item (set values):
      • 2FA.is_active = FALSE
      • 2FA.update_at = now()
  • Return 200
  • ...

Response

  • 200 if 2FA successful disable + 2FA_object_view
  • 4xx in other case


3- WS: ResetUser2FA (new EP) DONE

APIary

PATCH mithril/api/users/{user_id}/2fa{2fa_id}/actions/reset2fa 

Scope: `2fa:reset`

Purpose

Reset factor value by 2fa_id for user (via Admin-console).

Request parameters

  • user_id
  • 2fa_id

Logic WS

  • Validate token & scope
  • Validate user_id FK
  • Validate 2fa_id FK
  • Get 2FA item by 2fa_id  for non-blocked user by $.user_id

    SELECT *
    FROM authentication_factors AS 2FA
    	INNER JOIN user AS U
    		ON 2FA.user_id = U.id
    WHERE 
    	U.id = $.user_id
    		AND 2FA.id = $.2fa_id
    		AND U.is_active = TRUE
    		AND U.is_blocked = FALSE
    • If  exist 2FA item for user →  update 2FA item (set values) to logical status = RESET :
      • 2FA.factor = NULL
      • 2FA.update_at = now()
  • Return 200
  • ...

Response

  • 200 if 2FA successful reset  + 2FA_object_view
  • 4xx in other case



4- WS: UnBlockUser (new EP) DONE

APIary

unblock-user

PATCH mithril/api/users/{id}/actions/unblock 

Scope: `user:unblock`

Purpose

Unblock user (via Admin-console).

Request parameters

  • user_id

Logic WS

  • Validate token & scope
  • Validate user id & user status
  • Update user (set values) by $.user_id
    • is_blocked = FALSE
    • update_at = now()
  • Return 200
  • ...

Response

  • 200 if user successful unblock  + user_object_view
  • 4xx in other case



5- WS: BlockUser (new EP)  DONE

APIary

block-user

PATCH mithril/api/users/{id}/actions/block 

Scope: `user:block`

Purpose

Unblock user (via Admin-console).

Request parameters

  • user_id
  • block_reason

Logic WS

  • Validate token & scope
  • Validate user id & user status
  • Update user (set values) by $.user_id
    • is_blocked = TRUE
    • block_reason = $.block_reason
    • update_at = now()
  • Return 200
  • ...

Response

  • 200 if user successful block  + user_object_view
  • 4xx in other case


6- WS: Create Token (Modification) DONE

APIary

POST mithril/api/tokens 

Scope: <NO SCOPE>

Purpose

Add handling logic for return different type of tokens by 2FA_Statuses

Request parameters

  • grant_type = "password"
  • email
  • password
  • client_id
  • scope = "app:authorize"

Logic WS

  • Validate token & scope
  • Check login exist
    • If login exist
      • Get user by $.email
      • Validate user.password = $.password
      • If invalid -  Update user (set values) by $.user_id
        • Increment `users.priv_settings.login_error_counter` (+1)
        • If `users.priv_settings.login_error_counter` > USER_LOGIN_ERROR_MAX
          • Blocked user - update user (set values) by $.user_id
            • is_blocked = TRUE
            • block_reason = "OTP verify attempts more then USER_LOGIN_ERROR_MAX"
            • updated_at = now()
  • Validate user status
  • Get active 2FA item for non-blocked user by $.user_id

    SELECT *
    FROM authentication_factors AS 2FA
    	INNER JOIN user AS U
    		ON 2FA.user_id = U.id
    WHERE 
    	U.id = $.user_id
    		AND U.is_active = TRUE
    		AND U.is_blocked = FALSE
    		AND 2FA.is_active = TRUE
    • If exist 2FA active item with empty factor
      • !!! TBD - Need get factor from user (WS: Set2FA)
    • If  exist 2FA active item for user with non-empty factor
      • go to new process with 2FA (return 2fa_access_token + code 201)
    • If not exist 2FA active item for user
      • go to standart process without 2FA (return access_token + code 201)

Response

  • 201 if token successful create & return
  • 4xx in other case


7- WS: Create Approvals (Modification) DONE

APIary

APIary

POST mithril/api/apps 

Scope: <NO SCOPE>

Purpose

Add handling logic for same type of tokens

Request parameters

  • token
  • client_id
  • redirect_uri
  • scope

Logic WS

  • Validate $.token
  • Get token by $.token
  • Extract token_type, user_id
  • Get user by user_id
  • Get active 2FA item for non-blocked user by $.user_id

    SELECT *
    FROM authentication_factors AS 2FA
    	INNER JOIN user AS U
    		ON 2FA.user_id = U.id
    WHERE 
    	U.id = $.user_id
    		AND U.is_active = TRUE
    		AND U.is_blocked = FALSE
    		AND 2FA.is_active = TRUE
    • If  not exist 2FA active item for user
      • If token_type = access_token - go to standard process create Approvals & return changed token (existing logic)
      • At any token_type - return 4xx error "" !!! TBD
    • If exist active 2FA
      • if token_type = access_token  - go to standard process create Approvals & return changed token (existing logic)
      • If token_type = 2fa_access_token - return 401 error

Response

  • 200 if token is correct & user grants approvals successful 
  • 4xx in other case



8- WS: Create OTP (New EP) DONE

APIary

PATCH mithril/api/users/{id}/actions/sendOTP 

Scope: <NO SCOPE>

Purpose

Create & Send new OTP item for user via current 2FA factor

Request parameters

  • user_id
  • token

Logic WS

  • Validate token (2fa_access_token) - ???? 
    • If invalid - return error 4xx 
  • Validate user id & user status
  • Get active 2FA item for non-blocked user by $.user_id

    SELECT *
    FROM authentication_factors AS 2FA	
    WHERE 
    	2FA.user_id = $.user_id
    		AND 2FA.is_active = TRUE
    • If not found - return 409 error "Not found 2FA data for user"
  • Extract type & factor from 2FA item for user
  • Invoke internal function `create OTP (key)`, for 2FA.type = SMS, with params:
    • key = 2FA.faсtor
    • Get result of call `create OTP()` as `OTP_value` 
  • Sending (delivery) OTP via channel communication 
    • for 2FA.type = SMS - via SMS gateway API
      • mobile phone = 2FA.factor
      • SMS text = OTP_value 
  • Return 200
  • ...

Internal logic for `create OTP()`

  • Processed OTP lists for $.key
    • Deactivate all active OTP items (NEW → CANCELED)
  • Create new OTP item
    • status = NEW
    • expired_at = now() + OTP_LIFETIME
    • value = generate number according OTP_LENGTH
    • updated_at = now()

Response

  • 200 if OTP successful create & send 
  • 4xx in other case

9- WS: Authorize via 2FA (New EP) DONE

APIary

PATCH mithril/api/users/{id}/actions/authorize2fa 

Scope: <NO SCOPE>

Purpose

Verify OTP & prolongation authentication process (return access_token for getting approvals) .

Request parameters

  • user_id
  • otp
  • token

Logic WS

  • Validate token (2fa_access_token) - ???? 
    • If invalid - return error 4xx 
  • Validate user id & user status
  • Get active 2FA item for non-blocked user by $.user_id

    SELECT *
    FROM authentication_factors AS 2FA
    WHERE 
    	2FA.user_id = $.user_id
    		AND 2FA.is_active = TRUE
    • If not found - return 409 error "Not found 2FA data for user"
  • Extract type & factor from 2FA item for user
  • Invoke internal function `verify OTP (key, code)`, for 2FA.type = SMS, with params:
    • key = 2FA.faсtor
    • code = $.otp
  • Get result of call `verify OTP()`  
  • If result = VERIFIED
    • Update user (set values) by $.user_id
      • users.priv_settings.otp_error_counter = 0

    • Update 2fa_access_token (set `tokens.details.used`=true)
    • Create & return new access_token (as a existing standart process without 2FA)
    • Return 200
  • If result = UNVERIFIED
    • Update user (set values) by $.user_id
      • Increment `users.priv_settings.otp_error_counter` (+1)
    • If `users.priv_settings.otp_error_counter` > USER_OTP_ERROR_MAX
      • Blocked user - update user (set values) by $.user_id
        • is_blocked = TRUE
        • block_reason = "OTP verify attempts more then USER_OTP_ERROR_MAX"
        • updated_at = now()
    • return 401 error

Internal logic for `verify OTP()`

  • Find 1 active OTP (status = NEW) for $.key
    • If not found - return 409 error "Not found active OTP" - ??? or 401 ?
    • If found -  increment  `attempts_count` (+1) for this OTP
      • If OTP.code = $.code - update OTP item:
        • OTP status  ( NEW → VERIFIED)
        • updated_at = now()
      • If (OTP.code <> $.code ) AND (OTP.attempts_countOTP_ERROR_MAX 
        • Update OTP item
          • status ( NEW → UNVERIFIED)
          • updated_at = now()

Response

  • 200 if OTP successful create & send 
  • 4xx in other case

10- WS: CreateUser2FA (new EP) DONE

APIary

POST mithril/api/users/{user_id}/2fa

Scope: `2fa:write`

Purpose

Create factor item for user (after RESET of 2fa)

Request parameters

  • token
  • user_id
  • 2fa_id
  • type
  • factor

Logic WS

  • Validate token & scope
  • Validate user_id FK 
  • Validate user status
  • Insert new record in `authentication_factors` 
    • user_id = $.user_id
    • type = $.type
    • factor = $.factor
    • is_active = TRUE
    • update_at = now()
  • Return 201

Response

  • 201 if 2FA successful reset  + 2FA_object_view
  • 4xx in other case

xx- WS: Set factor user 2FA (New EP) TO DELETE

APIary

PATCH mithril/api/users/{user_id}/2fa{2fa_id}/actions/set_factor 

Scope: `2fa:set_factor`

Purpose

Update value of factor (after RESET of 2fa)

Request parameters

  • user_id
  • 2fa_id
  • new_factor
  • token


Logic WS

  • Validate token & scope
  • Validate user_id FK
  • Validate 2fa_id FK
  • Get 2FA item by 2fa_id  for non-blocked user by $.user_id

    SELECT *
    FROM authentication_factors AS 2FA
    	INNER JOIN user AS U
    		ON 2FA.user_id = U.id
    WHERE 
    	U.id = $.user_id
    		AND 2FA.id = $.2fa_id
    		AND U.is_active = TRUE
    		AND U.is_blocked = FALSE
    • If  (exist 2FA item for user) AND (token_type = access_token_type) AND (2FA.factor <> "" AND  2FA.factor <> NULL) - update 2FA item (set values) to logical status = ACTIVE :
      • 2FA.factor = new_factor
      • 2FA.update_at = now()
    • If  (exist 2FA item for user) AND (token_type = 2fa_access_token_type) AND (2FA.factor = "" OR  2FA.factor = NULL) - update 2FA item (set values) to logical status = ACTIVE :
      • 2FA.factor = new_factor
      • 2FA.update_at = now()
    • Else return 401 error 
  • Return 200
  • ...

Response

  • 200 if 2FA successful reset  + 2FA_object_view
  • 4xx in other case



11- PROC: [AUTO] Terminate OTP (new PROC) DONE

Purpose

AUTO-Terminating expired OTP.

Logic

  • Fetch records from `otp`, where:

    • status = 'NEW'
    • expired_at < now() 
  • Update (Set values) OTP items 
    1. status  =  EXPIRED
    2. updated_at = now()




12- WS: Get user 2FA List (new EP) DONE

APIary

GET mithril/api/users/{id}/2fa/

Scope: `2fa:read`

Purpose

Get 2FA items list for user (via Admin-console).

Request parameters

  • user_id
  • type

Logic WS

  • Validate token & scope
  • Validate user id & user status
  • Get data by filters 

    SELECT *
    FROM authentication_factors AS 2FA
    WHERE
    	(2FA.user_id = $.user_id OR $.user_id IS NULL)
    		AND (2FA.type = $.type OR $.type IS NULL)
  • Return 200
  • ...

Response

  • 200 if get list successful  + 2FA_list_view
  • 4xx in other case




13- WS: Get user 2FA by ID  (new EP) DONE

APIary

GET mithril/api/users/{user_id}/2fa/{2fa_id}

Scope: `2fa:read`

Purpose

Get 2FA item by id (via Admin-console).

Request parameters

  • 2fa_id

Logic WS

  • Validate token & scope
  • Validate 2fa id 
  • Validate user_id FK
  • Get data by id 

    SELECT *
    FROM authentication_factors AS 2FA
    WHERE 2FA.id = $.id
    		AND 2FA.user_id = $.user_id
  • Return 200
  • ...

Response

  • 200 if get list successful + 2FA_object_view
  • 4xx in other case







ЕСОЗ - публічна документація