Architecture

Abstracting the authentication process

First let’s start by taking a closer look at how web authentication works. Every authentication process can be abstracted as a Finite State Machine.

On a high level, we start in the unauthenticated state, the user sends the application their credentials, optionally the multi-factor authentication (MFA) code, and if both checks pass, we reach the authenticated state. A typical modern web application will looks like the following in a diagram:

[*] --> Unauthenticated : Open login page

state Unauthenticated
state "MFA required" as MFA_required
state "Login failed" as login_failed
state Authenticated

Authenticated --> [*]

state "Login check" as login <<sdlreceive>>
state "Is MFA enabled?" as mfa_enabled <<sdlreceive>>
state "Is MFA code correct?" as mfa_correct <<sdlreceive>>

state login_check <<choice>>
state mfa_enabled_check <<choice>>
state mfa_correct_check <<choice>>


Unauthenticated --> login : Log in
login --> login_check
login_check --> mfa_enabled : Correct credentials
login_check --> login_failed : Bad credentials


mfa_enabled --> mfa_enabled_check
mfa_enabled_check --> MFA_required : MFA enabled
mfa_enabled_check --> Authenticated : MFA disabled

MFA_required -->  mfa_correct
mfa_correct --> mfa_correct_check
mfa_correct_check --> login_failed : Failed MFA
mfa_correct_check --> Authenticated : Passed MFA

Basic concepts in Raider

Now let’s zoom in and look at the details. Instead of dealing with the states (Unauthenticated, Login failed, MFA required, and Authenticated), we define the concept of stages, which describes the information exchange between the client and the server containing one request and the respective response.

The example below shows a closer look of the authentication process for an imaginary web application:

state User {
  User: Username
  User: Password
}

state Factors {
Factors: SMS code
Factors: E-mail confirmation
Factors: TOTP
Factors: Biometrics
}



state Initialization {
  state Request0
  state Response0
  Request0 -[#orange]-> Response0
  Response0 --> outputs0
  state outputs0 <<expansionOutput>>
  Response0 : CSRF Token
  Response0 : Session cookie
}


state Login {
  state inputs1 <<expansionInput>>
  state outputs1 <<expansionOutput>>
  state Request1
  state Response1
  inputs1 --> Request1
  outputs0 --> inputs1
  Request1 -[#orange]-> Response1
  Request1: CSRF Token
  Request1: Session cookie
  Request1: Username
  Request1: Password
  Response1 --> outputs1
  Response1: CSRF Token
  Response1: Session cookie
  Response1: User cookie
}

User -[#blue]-> inputs1


state "Multi-factor authentication" as MFA {
  state inputs2 <<expansionInput>>
  state outputs2 <<expansionOutput>>
  state Request2
  Request2: CSRF Token
  Request2: Session cookie
  Request2: User cookie
  state Response2
  Response2: Session cookie
  Response2: User cookie
  Response2: MFA passed cookie
  Request2 -[#orange]-> Response2
  outputs1 --> inputs2
  inputs2 --> Request2
  Response2 --> outputs2

}

[*] --> Initialization

Initialization -[#green]-> Login


Factors -[#blue]-> inputs2

Login -[#green]> MFA : MFA enabled
Login -[#green]-> Authenticated : MFA disabled

state Authenticated
state "Login failed" as login_failed
state "MFA failed" as MFA_failed

Login -left[#green]-> login_failed : Bad credentials

MFA -[#green]-> Authenticated : MFA passed
MFA -left[#green]-> MFA_failed

Authenticated --> [*]

To describe the authentication process from the example defined above, we need three stages. The first one, Initialization, doesn’t have any inputs, but creates the Session cookie and the CSRF token as outputs.

Those outputs are passed to the next stage, Login, together with user credentials. A request is built with those pieces of information, and the new outputs are generated. In this case we have the new CSRF token, an updated session cookie, and a new cookie identifying the user: user cookie.

Depending on whether MFA is enabled or not, the third stage Multi-factor authentication might be skipped or executed. If it’s enabled, the outputs from the previous stage get passed as inputs to this one, the user is asked to input the next Factor, and a new cookie is set proving the user has passed the checks and is properly authenticated.

In Raider, stages are implemented using Flow objects. The authentication process consists of a series of Flows connected to each other. Each one accepts inputs and generates outputs. In addition to that, Flow objects implement Operations which can be used to run various actions upon receiving the response, but most importantly they’re used to control the authentication process by conditionally or unconditionally defining the next stage. So for example one can jump to stage X if the HTTP response code is 200 or to stage Y if it’s 403.

skinparam componentStyle rectangle

package "Flow0" {
  frame "Stage0" {
  [Request0] -down- [Response0]
  }

  component outputs0 [
  output_0
  output_1
  ...
  output_n
  ]

  component operations0 [
  operation_0
  operation_1
  ...
  operation_n
  ]

  Response0 --> outputs0
  note top of outputs0 : Outputs

  outputs0 -right-> operations0
  note top of operations0 : Operations

}


package "Flow1" {
  frame "Stage1" {
  [Request1] -down- [Response1]
  }

  component outputs1 [
  output_0
  output_1
  ...
  output_n
  ]

  component inputs1 [
  input_0
  input_1
  ...
  input_n
  ]

  component operations1 [
  operation_0
  operation_1
  ...
  operation_n
  ]

  Response1 --> outputs1
  note top of outputs1 : Outputs
  note left of inputs1 : Inputs

  outputs1 -right-> operations1
  note top of operations1 : Operations

}

operations0 -> Flow1
outputs0 -> inputs1
inputs1 -> Request1

Inputs and outputs are often the same object, and you may want to update its value from one Flow to the next (for example the CSRF token changes for every stage). This was implemented in Raider using Plugins.

Plugins are pieces of code that can act as inputs for the HTTP requests to be sent, and/or as outputs from the HTTP responses. They are used to facilitate the information exchange between Flows. Raider provides the user the option to write new plugins with a small piece of hylang code.

Once the response is received, the Operations will be executed. The primary function of operations is to define which Flow comes next. But they can do anything, and Raider makes it easy to write new operations.