Table of Contents

Single Sign On

This is a project driven by @samp20 to build an SSO system for all Hackspace services.

Architecture

The current proposed high level architecture is a Python Flask application to handle logins with an Ory Hydra frontend to provide the SSO.

Login flows

There will be a few login flows supported:

Additionally a 2nd factor TOTP can be added if desired.

Password login won't be supported to begin with unless there is a strong demand for it.

Email login

Email login will send a “magic link” to your registered email address. When clicked this will log you in to the original page you were on, not the page opened when clicking the link. This will allow you to login on your phone while clicking the email on your desktop for example. This could also work on the Hackspace portal if desired.

In order to avoid mis-clicking a “magic link” triggered by a potential attacker, both the email and login page should display a code so the member can check they are clicking the correct link.

Keyfob/card login

This will be a slight change to our current login method, requiring you to enter your email first before scanning your keyfob/card. This is because the keyfobs aren't particularly secure and could be easily cloned. By treating them more like a PIN we can disable keyfob login after so many attempts for that member.

An additional security measure will be required to ensure these keyfob logins only come from the Hackspace network. For now an IP allowlist should be sufficient, along with a global lockout if a significant number of keyfob login attempts are spotted. It is recognised that IP addresses can theoretically be spoofed, but quite difficult in practice. The global lockout would be a nuclear countermeasure in the extremely rare instance someone does figure this out.

Login state machine

Logins shall be handled by a state machine stored in the Database which is created upon receiving a login flow from Hydra. Each web request will process the state machine in the following order:

  1. Create a state object for the current state
  2. Validate that object against the current request (e.g. checking form fields)
  3. If validated then use a transition table to update the current state
  4. Use the current state to render the appropriate form (if the validation had failed then this will render any error messages to the user too)

The following states will be supported:

Here is the proposed transition table:

Current state Condition (assumes validated) Next state Comments
None Email entry
Email entry email submitted Magic link We treat all emails as valid to prevent this page being used to check if an email exists
Email entry passkey submitted Finish
Email entry portal cookie set Keyfob scan
Magic link member has TOTP TOTP
Magic link member doesn't have TOTP Finish While the login might succeed, the OAuth application may still reject the login if additional security is required
Keyfob scan Member has TOTP TOTP If a member has setup TOTP then we assume they always want the extra security
Keyfob scan member doesn't have TOTP Finish
Keyfob scan Email login requested Magic link If a member gets their keyfob locked out or doesn't have it on them then this gives another option
TOTP Finish

Below is the same table in diagram form:

projects:sso_transitions.png

As well as the explicit transitions there will also be the option to reset everything back to the start.

Permissions model

Permissions shall be granted through OAuth scopes or other custom claims. As these claims can sometimes be application specific, a general purpose approach is proposed using Members, Roles and ClaimSets with the following relationships:

A Role describes a high-level role a member has, for example “onboarding”. A ClaimSet describes the specific OAuth claims associated with that role. The reason for separating ClaimSets from Roles is to be able to limit a ClaimSet to a single OAuth client without requiring a member to join multiple Roles if that Role involves multiple clients.

An example ClaimSet for Grafana access to the “viewer” team may look like the following:

{
    "scope": ["openid", "email"],
    "groups": ["viewer"]
}

This ClaimSet would be restricted to the Grafana OAuth client in order to avoid granting the “openid” scope to other clients and inadvertently giving access to them.