Member Portal V2
This is a project driven by @samp20 to build a new member portal to provide Single-Sign-On (SSO) to other Hackspace services.
Architecture
The system is based on a Python Flask application with PostgreSQL as the backend database.
Below is a diagram showing the system dependencies:
Left to do
Required for initial demo:
- Portal homepage
- Hardcode OAuth permissions for demo
- Logout
- Host demo under Hackspace beta domain
- Sample OpenID compatible application (suggestions welcome)
Shortly after:
- Rate limiter
- By attempted email address
- By IP address
- Object cleanup system
Future work:
- Permissions system
- PKCE
- View/edit personal details
- View/delete active sessions
- External logins (keyfob)
- Admin pages
- Clients
- Roles
- Members
Project layout
portal/: The whole application.forms/: WTForms.static/: Static files, e.g. css.systems/: Core systems, e.g. Session, OAuth, JWK.templates/: Jinja templates.views/: Flask Blueprints with routes. Glue between templates and systems.__init__.py: Application entrypoint.extensions.py: Instantiate extensions and systems.models.py: SQLAlchemy models.
tests/: Tests. Mainly for systems.
Systems
Systems will follow a similar style to Flask extensions. Flask requires that extensions don't store state directly on the class itself, but instead obtain it from the current_app. This allows multiple applications to be instantiated, for example when unit testing.
class _State: def __init__(self, app: Flask): self.greeting: str = app.config["MYSYSTEM_GREETING"] class MySystem: def __init__(self, other: OtherSystem, app: Flask|None): self.other = other if app is not None: self.init_app(app) def init_app(self, app: Flask): state = _State(app) app.extensions["hs.portal.my_system"] = state @property def _state(self) -> _State: state = current_app.extensions["hs.portal.my_system"] return state def get_greeting(self) -> str: return self._state.greeting
Testing
We will use pytest for our testing. We should aim to test each system in isolation.
Some code will require an active request to test. We can create test endpoints to satisfy these instead of using the fully templated ones.
For now we can probably use an in-memory SQLite database for testing. We may need to switch to a proper PostgreSQL database if we start to depend on DB specific features, at which point we'll need to clean the database before every test run.
Models
Tokens
A common pattern that's required is to store an external reference to a table row in a secure way. The pattern we use is to store an id of type UUID and a token_hash of type string in the table. The external token will refer to this using the string format {id.hex}:{token}. The ID is used to select the correct row and the SHA256 of token is compared against token_hash. The token is required as a layer of security as we cannot guarantee the id generation is cryptographically secure. It also allows us to rotate the token while still referring to the same row, for example cookie rotation.