This is a project driven by @samp20 to build an SSO system for all Hackspace services.
The current proposed high level architecture is a Python Flask application to handle logins with an Ory Hydra frontend to provide the SSO.
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 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.
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.
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:
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:
As well as the explicit transitions there will also be the option to reset everything back to the start.
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.