Skip to content

Commit

Permalink
Do not allow using device token to login if no secondary authenticato…
Browse files Browse the repository at this point in the history
…r exist

ref DEV-2263
  • Loading branch information
louischan-oursky committed Nov 8, 2024
2 parents db3e9b8 + 46f3df2 commit b6c0cf3
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 37 deletions.
226 changes: 226 additions & 0 deletions e2e/tests/login/device_token/disabled_if_no_2fa/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
name: Login - Device Token - Disabled if no secondary authenticator
authgear.yaml:
override: |
authentication:
identities:
- login_id
primary_authenticators:
- password
secondary_authenticators:
- password
secondary_authentication_mode: required
identity:
login_id:
keys:
- type: username
before:
- type: user_import
user_import: users.json
steps:
- action: "create"
input: |
{
"type": "login",
"name": "default"
}
output:
result: |
{
"action": {
"type": "identify",
"data": {
"type": "identification_data",
"options": "[[array]]"
}
}
}
- action: input
input: |
{
"identification": "username",
"login_id": "e2e_login_device_token"
}
output:
result: |
{
"action": {
"type": "authenticate",
"data": {
"device_token_enabled": false,
"options": [
{
"authentication": "primary_password"
}
],
"type": "authentication_data"
}
}
}
- action: input
input: |
{
"authentication": "primary_password",
"password": "password"
}
output:
result: |
{
"action": {
"type": "authenticate",
"data": {
"device_token_enabled": true,
"options": [
{
"authentication": "secondary_password"
}
],
"type": "authentication_data"
}
}
}
- action: input
input: |
{
"authentication": "secondary_password",
"password": "password",
"request_device_token": true
}
output:
result: |
{
"action": {
"type": "finished"
}
}
# Login again. Expect 2fa is skipped by using device token
- action: "create"
input: |
{
"type": "login",
"name": "default"
}
output:
result: |
{
"action": {
"type": "identify",
"data": {
"type": "identification_data",
"options": "[[array]]"
}
}
}
- action: input
input: |
{
"identification": "username",
"login_id": "e2e_login_device_token"
}
output:
result: |
{
"action": {
"type": "authenticate",
"data": {
"device_token_enabled": false,
"options": [
{
"authentication": "primary_password"
}
],
"type": "authentication_data"
}
}
}
- action: input
input: |
{
"authentication": "primary_password",
"password": "password"
}
output:
result: |
{
"action": {
"type": "finished"
}
}
# Remove the 2fa.
- action: query
query: |
WITH authenticator_ids AS (
SELECT id FROM _auth_authenticator
WHERE app_id = '{{ .AppID }}'
AND kind = 'secondary'
), del_1 AS (
DELETE FROM _auth_authenticator_password
WHERE id IN (SELECT id FROM authenticator_ids)
)
DELETE FROM _auth_authenticator
WHERE id IN (SELECT id FROM authenticator_ids);
# Login again, should be blocked because 2fa is required.
- action: "create"
input: |
{
"type": "login",
"name": "default"
}
output:
result: |
{
"action": {
"type": "identify",
"data": {
"type": "identification_data",
"options": "[[array]]"
}
}
}
- action: input
input: |
{
"identification": "username",
"login_id": "e2e_login_device_token"
}
output:
result: |
{
"action": {
"type": "authenticate",
"data": {
"device_token_enabled": false,
"options": [
{
"authentication": "primary_password"
}
],
"type": "authentication_data"
}
}
}
- action: input
input: |
{
"authentication": "primary_password",
"password": "password"
}
output:
error: |
{
"name": "Invalid",
"reason": "InvariantViolated",
"message": "no authenticator",
"code": 400,
"info": {
"FlowType": "login",
"cause": {
"kind": "NoAuthenticator"
}
}
}
19 changes: 19 additions & 0 deletions e2e/tests/login/device_token/disabled_if_no_2fa/users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"identifier": "preferred_username",
"records": [
{
"preferred_username": "e2e_login_device_token",
"name": "Login",
"password": {
"type": "bcrypt",
"password_hash": "$2y$10$/wLavnCmYGP/zzpw/mR1iOK5y5hGyrEFJtmaIbvFf9VA6l2O4NMKO"
},
"mfa": {
"password": {
"type": "bcrypt",
"password_hash": "$2y$10$/wLavnCmYGP/zzpw/mR1iOK5y5hGyrEFJtmaIbvFf9VA6l2O4NMKO"
}
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ func init() {
// NodeDoConsumeRecoveryCode (MilestoneDidAuthenticate)

type IntentLoginFlowStepAuthenticate struct {
FlowReference authflow.FlowReference `json:"flow_reference,omitempty"`
JSONPointer jsonpointer.T `json:"json_pointer,omitempty"`
StepName string `json:"step_name,omitempty"`
UserID string `json:"user_id,omitempty"`
Options []AuthenticateOption `json:"options"`
FlowReference authflow.FlowReference `json:"flow_reference,omitempty"`
JSONPointer jsonpointer.T `json:"json_pointer,omitempty"`
StepName string `json:"step_name,omitempty"`
UserID string `json:"user_id,omitempty"`
Options []AuthenticateOption `json:"options"`
DeviceTokenEnabled bool `json:"device_token_enabled"`
}

var _ authflow.TargetStep = &IntentLoginFlowStepAuthenticate{}
Expand Down Expand Up @@ -91,12 +92,13 @@ func NewIntentLoginFlowStepAuthenticate(ctx context.Context, deps *authflow.Depe
}
step := i.step(current)

options, err := getAuthenticationOptionsForLogin(ctx, deps, flows, i.UserID, step)
options, deviceTokenEnabled, err := getAuthenticationOptionsForLogin(ctx, deps, flows, i.UserID, step)
if err != nil {
return nil, err
}

i.Options = options
i.DeviceTokenEnabled = deviceTokenEnabled
return i, nil
}

Expand All @@ -115,9 +117,6 @@ func (i *IntentLoginFlowStepAuthenticate) CanReactTo(ctx context.Context, deps *
}
step := i.step(current)

deviceTokenIndex := i.deviceTokenIndex(step)
deviceTokenEnabled := deviceTokenIndex >= 0

_, _, deviceTokenInspected := authflow.FindMilestoneInCurrentFlow[MilestoneDeviceTokenInspected](flows)

authenticationMethodSelected := false
Expand All @@ -139,7 +138,7 @@ func (i *IntentLoginFlowStepAuthenticate) CanReactTo(ctx context.Context, deps *
createAuthenticatorMilestones := authflow.FindAllMilestones[MilestoneFlowCreateAuthenticator](flows.Root)

switch {
case deviceTokenEnabled && !deviceTokenInspected:
case i.DeviceTokenEnabled && !deviceTokenInspected:
// Inspect the device token
return nil, nil
case !authenticationMethodSelected:
Expand All @@ -161,7 +160,7 @@ func (i *IntentLoginFlowStepAuthenticate) CanReactTo(ctx context.Context, deps *
FlowRootObject: flowRootObject,
JSONPointer: i.JSONPointer,
Options: i.Options,
DeviceTokenEnabled: deviceTokenEnabled,
DeviceTokenEnabled: i.DeviceTokenEnabled,
ShouldBypassBotProtection: shouldBypassBotProtection,
BotProtectionCfg: deps.Config.BotProtection,
}, nil
Expand All @@ -170,7 +169,7 @@ func (i *IntentLoginFlowStepAuthenticate) CanReactTo(ctx context.Context, deps *
// We expect the selected authentication method to be authenticated before this intent becomes input reactor again.
panic(fmt.Errorf("unauthenticated"))

case deviceTokenEnabled && !deviceTokenCreatedIfRequested:
case i.DeviceTokenEnabled && !deviceTokenCreatedIfRequested:
// We look at the current input to see if device token is request.
// So we do not need to take another input.
return nil, nil
Expand All @@ -189,9 +188,6 @@ func (i *IntentLoginFlowStepAuthenticate) ReactTo(ctx context.Context, deps *aut
}
step := i.step(current)

deviceTokenIndex := i.deviceTokenIndex(step)
deviceTokenEnabled := deviceTokenIndex >= 0

_, _, deviceTokenInspected := authflow.FindMilestoneInCurrentFlow[MilestoneDeviceTokenInspected](flows)

authenticationMethodSelected := false
Expand All @@ -211,7 +207,7 @@ func (i *IntentLoginFlowStepAuthenticate) ReactTo(ctx context.Context, deps *aut
_, _, nestedStepsHandled := authflow.FindMilestoneInCurrentFlow[MilestoneNestedSteps](flows)

switch {
case deviceTokenEnabled && !deviceTokenInspected:
case i.DeviceTokenEnabled && !deviceTokenInspected:
return authflow.NewSubFlow(&IntentInspectDeviceToken{
UserID: i.UserID,
}), nil
Expand Down Expand Up @@ -296,9 +292,9 @@ func (i *IntentLoginFlowStepAuthenticate) ReactTo(ctx context.Context, deps *aut
return nil, authflow.ErrIncompatibleInput
case !authenticated:
panic(fmt.Errorf("unauthenticated"))
case deviceTokenEnabled && !deviceTokenCreatedIfRequested:
case i.DeviceTokenEnabled && !deviceTokenCreatedIfRequested:
return authflow.NewSubFlow(&IntentCreateDeviceTokenIfRequested{
JSONPointer: authflow.JSONPointerForOneOf(i.JSONPointer, deviceTokenIndex),
JSONPointer: authflow.JSONPointerForOneOf(i.JSONPointer, i.deviceTokenIndex(step)),
UserID: i.UserID,
}), nil
case !nestedStepsHandled:
Expand All @@ -313,14 +309,6 @@ func (i *IntentLoginFlowStepAuthenticate) ReactTo(ctx context.Context, deps *aut
}

func (i *IntentLoginFlowStepAuthenticate) OutputData(ctx context.Context, deps *authflow.Dependencies, flows authflow.Flows) (authflow.Data, error) {
current, err := i.currentFlowObject(deps)
if err != nil {
return nil, err
}
step := i.step(current)

deviceTokenIndex := i.deviceTokenIndex(step)
deviceTokenEnabled := deviceTokenIndex >= 0

options := []AuthenticateOptionForOutput{}
for _, o := range i.Options {
Expand All @@ -329,7 +317,7 @@ func (i *IntentLoginFlowStepAuthenticate) OutputData(ctx context.Context, deps *

return NewStepAuthenticateData(StepAuthenticateData{
Options: options,
DeviceTokenEnabled: deviceTokenEnabled,
DeviceTokenEnabled: i.DeviceTokenEnabled,
}), nil
}

Expand Down
Loading

0 comments on commit b6c0cf3

Please sign in to comment.