Adaptive Authentication

Castle's Adaptive Authentication tells you whether to allow access, initiate a second factor of authentication, or log out the user.

The authenticate endpoint returns a recommended action:

  • allow: Let the user in
  • challenge: Trigger higher level of authentication using SMS, email or an external app
  • deny: Lock account, log out all sessions, and reset password
verdict = castle.authenticate(
  event: '$login.succeeded',
  user_id: user.id,
  user_traits: {
    email: 'johan@castle.io',
    created_at: '2015-02-23T22:28:55.387Z'
  }
)

case verdict[:action]
when 'allow'
  # Fully authenticated. Let the user proceed
when 'challenge'
  # Additional authentication required
when 'deny'
  # Failed authentication. Log out the user
end
verdict = castle.authenticate(
    {
        'event': '$login.succeeded',
        'user_id': user.id,
        'user_traits': {
            'email': 'johan@castle.io',
            'created_at': '2015-02-23T22:28:55.387Z'
        }
    }
)

if verdict['action'] == 'allow':
    # Fully authenticated. Let the user proceed
elif verdict['action'] == 'challenge':
    # Additional authentication required
else verdict['action'] == 'deny':
    # Failed authentication. Log out the user
<?
$verdict = Castle::authenticate(
  array(
    'event' => '$login.succeeded',
    'user_id' => $user->id,
    'user_traits' => array(
      'email' => 'johan@castle.io',
      'created_at' => '2015-02-23T22:28:55.387Z'
    )
  )
);

if ($verdict->action == 'allow') {
  // Fully authenticated. Let the user proceed
} else if ($verdict->action == 'challenge') {
  // Additional authentication required
} else if ($verdict->action == 'deny') {
  // Failed authentication. Lock account
}
CastleContext context = Castle.contextBuilder()
    .fromHttpServletRequest(req)
    .build();

Verdict verdict = Castle.client().authenticate(CastleMessage.builder("$login.succeeded")
    .context(context)
    .userId(userId)
    .userTraits(ImmutableMap.builder()
        .put("email", "johan@castle.io")
        .put("created_at", "2015-02-23T22:28:55.387Z")
        .build())
    .build()
);

switch (verdict.getAction()) {
    case ALLOW: {
      // Fully authenticated. Let the user proceed
    }
    break;
    case CHALLENGE: {
      // Additional authentication required
    }
    break;
    case DENY: {
      // Failed authentication. Lock account
    }
    break;
}
curl https://api.castle.io/v1/authenticate \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
  {
    "event": "$login.succeeded",
    "user_id": "1234",
    "context": {
      "client_id": "a97b492d-dcc3-4fc1-87d6-65682955afa5",
      "ip": "37.46.187.90",
      "headers": {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
        "Accept": "text/html",
        "Accept-Language": "en-us,en;q=0.5"
      }
    },
    "user_traits": {
      "email": "johan@castle.io",
      "created_at": "2015-02-23T22:28:55.387Z"
    }
  }'

When action is challenge or deny, a webhook will also be triggered. It's recommended that you use the webhook to generate a notification to the end-user to make them aware of any bad actor trying to access their account.

Step 1. Blocking malicious logins

When action is deny, simply log out the current session and wait for the $incident.confirmed webhook. You must not reset a user account based on the deny response, but instead do it in the webhook handler.

Once the the user has reset their password, you track $incident.mitigated in order to flag and remove the malicious device from the user's account.

castle.track(
  event: '$incident.mitigated',
  user_id: user.id
)
castle.track(
    {
        'event': '$incident.mitigated',
        'user_id': user.id
    }
)
<?
Castle::track(
  array(
    'event' => '$incident.mitigated',
    'user_id' => $user->id
  )
);
CastleContext context = Castle.contextBuilder()
    .fromHttpServletRequest(req)
    .build();

Castle.client().track(CastleMessage.builder("$incident.mitigated")
    .context(context)
    .userId(userId)
    .build()
);
curl https://api.castle.io/v1/track \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
  {
    "event": "$incident.mitigated",
    "user_id": "1234",
    "context": {
      "client_id": "a97b492d-dcc3-4fc1-87d6-65682955afa5",
      "ip": "37.46.187.90",
      "headers": {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
        "Accept": "text/html",
        "Accept-Language": "en-us,en;q=0.5"
      }
    }
  }'

Step 2. Challenging suspicious logins

When action is challenge, redirect the user to a second page and present them with a verification flow, such as a PIN code sent over email.

Track $challenge.requested to tell Castle that the challenge process has started.

castle.track(
  event: '$challenge.requested',
  user_id: user.id
)
castle.track(
    {
        'event': '$challenge.requested',
        'user_id': user.id
    }
)
<?
Castle::track(
  array(
    'event' => '$challenge.requested',
    'user_id' => $user->id
  )
);
castle.track("$challenge.requested", user_id);
curl https://api.castle.io/v1/track \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
  {
    "event": "$challenge.requested",
    "user_id": "1234",
    "context": {
      "client_id": "a97b492d-dcc3-4fc1-87d6-65682955afa5",
      "ip": "37.46.187.90",
      "headers": {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
        "Accept": "text/html",
        "Accept-Language": "en-us,en;q=0.5"
      }
    }
  }'

When the user completes the challenge, track $challenge.succeeded to lower the risk for the current session.

castle.track(
  event: '$challenge.succeeded',
  user_id: user.id
)
castle.track(
    {
        'event': '$challenge.succeeded',
        'user_id': user.id
    }
)
<?
Castle::track(
  array(
    'event' => '$challenge.succeeded',
    'user_id' => $user->id
  )
);
CastleContext context = Castle.contextBuilder()
    .fromHttpServletRequest(req)
    .build();

Castle.client().track(CastleMessage.builder("$challenge.succeeded")
    .context(context)
    .userId(userId)
    .build()
);
curl https://api.castle.io/v1/track \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
  {
    "event": "$challenge.succeeded",
    "user_id": "1234",
    "context": {
      "client_id": "a97b492d-dcc3-4fc1-87d6-65682955afa5",
      "ip": "37.46.187.90",
      "headers": {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
        "Accept": "text/html",
        "Accept-Language": "en-us,en;q=0.5"
      }
    }
  }'

Alternatively, track $challenge.failed to increase the risk for similar contexts.

castle.track(
  event: '$challenge.failed',
  user_id: user.id
)
castle.track(
    {
        'event': '$challenge.failed',
        'user_id': user.id
    }
)
<?
Castle::track(
  array(
    'event' => '$challenge.failed',
    'user_id' => $user->id
  )
);
CastleContext context = Castle.contextBuilder()
    .fromHttpServletRequest(req)
    .build();

Castle.client().track(CastleMessage.builder("$challenge.failed")
    .context(context)
    .userId(userId)
    .build()
);
curl https://api.castle.io/v1/track \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
  {
    "event": "$challenge.failed",
    "user_id": "1234",
    "context": {
      "client_id": "a97b492d-dcc3-4fc1-87d6-65682955afa5",
      "ip": "37.46.187.90",
      "headers": {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
        "Accept": "text/html",
        "Accept-Language": "en-us,en;q=0.5"
      }
    }
  }'