-
Notifications
You must be signed in to change notification settings - Fork 0
Description
TL;DR We should use JSON Web Tokens (JWTs)
Look at the implementation section for how we could implement it
https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/
https://www.jwt.io/
https://www.rfc-editor.org/rfc/rfc7519#section-10.1
https://www.rfc-editor.org/rfc/rfc8725
Description
I think our backend should be frontend agnostic, which means we should probably improve how we handle sessions. We could also share sessions between different applications across the CSSS. This will require us to rewrite how our authentication flow works. Since we rely on SFU's CAS system, we would just need to adapt how our sessions are created, managed, and destroyed.
Right now we hit the database every time we need to check sessions. With JWTs we remove that requirement.
Changes
NOTE: We can remove needing to check the database every time we want to verify someone since everything we need should be in the access token. Right now, our DB reads are super low, so we can keep our current version. The instructions below assume we'll still store the session/access tokens and check the database every time we need to, but we can rework that table into the refresh table instead of making an entirely new refresh_token table.
Backend
- Add secrets to create JWT tokens
- Modify the POST
/auth/loginendpoint- Instead of creating a simple session ID, we'll create two JWTs: Access token and Refresh token (explained below)
- Create a new endpoint, POST
/auth/refresh, which takes the refresh token in the body and verifies it with the backend- Once verified, an access token is generated and stored on the cookie
Database
-
Modify the
user_sessiontable- rename
session_id->access_token: holds the access token's JWT value- we can just check this value for the privileges the person has, instead of checking the database for their permissions
- rename
-
Make a new table
refresh_tokens, with columns:hashed_token(TEXT, primary key): the unique identifier of the token, used to match the token's valuescomputing_id(VARCHAR): the user's computing IDservice(VARCHAR): the CAS service used when creating the refresh token, for logging out of CASjti(TEXT): the unique JWT ID, generated from uuidv4 or somethingissued_at(INTEGER): date, as a UTC number, for when the token was generatedexpires_at(INTEGER): date, as a UTC number, for when the token should expirerevoked(BOOLEAN): true if the token has been revoked, false otherwiseip(TEXT): hashed IP address from the request which created this tokenos_name(TEXT): hashed value from the user agentos_version(TEXT): hashed value from the user agentbrowser(TEXT): hashed value from the user agent
Access token
- JWT with the claims
- iss: string, "https://sfucsss.org"
- sub: string, computing_id
- iat: integer, represents the UTC time of when the token was generated
- exp: integer, represents the UTC time of when the token should be expired (iat + 15 minutes)
- roles: array of string, contains all the privileges a user has
- could be
admin,user, etc - needs every single role and lower (e.g. an
adminshould also haveuserin its array)
- could be
- Created after a successful verification with SFU CAS
- Stored in the cookie under the key
session_id - Utilize
httpOnlyandsecure(already done) - Used to verify any sensitive actions
- Backend should verify that the JWT token is valid and the scopes can be read from the cookie
- If the token is expired, send a
HTTP 401withWWW-Authenticate=Bearer
- If the token is expired, send a
Refresh token
- JWT which expires after 7 days, with claims
- iss: string, "https://sfucsss.org"
- sub: string, the user's
computing_id - iat: integer, represents the UTC time of when the token was generated
- exp: integer, represents the UTC time of when the token should be expired (iat + 7 days)
- ip: hashed IP address the request came from
- ua: hashed the information from the
User-Agentheader of the request when the refresh token was generated - jti: string, unique UUID of this token
- Created in the backend, stored in the new DB table
refresh_token - When creating a new refresh token, delete/invalidate the old one
- Stored in local storage on the frontend
- Our client applications can send this to the backend, which allows us the create a new session without having to authenticate with SFU's CAS system again
- Backend needs to validate that everything in the refresh token matches what is stored in the database entry
- That means the request's IP and User-Agent need to be checked for VERY sensitive data (like writing to the database)
- Could make the restrictions looser by checking the if the IP address is in the same region (dunno how to check that)
- Browser version changes should be okay
- If the validation fails, immediately revoke that token and send back a 401
Frontend
- Refresh tokens should be stored in local storage
- Access token needs to be sent for each request, but only check it when the user needs to access sensitive data
- If a 401 is received, make an API request to POST
/api/auth/refreshwith the headerAuthorization: Bearer <refresh_token> - It is the frontend's responsibility to manage the refresh token once it is received
Login flow
- Frontend -> CAS
- CAS: ticket -> Frontend
- Frontend: /api/auth/login w/ ticket & service -> Backend
- Backend: /serviceValidate?... -> CAS
- CAS : user info -> Backend
- Backend:
6.1. creates tokens, stores the refresh token in the database and
6.2. sends refresh token in the response, sets access token in the cookie - Frontend:
7.1. uses the access token whenever you need something sensitive or to access the CSSS's APIs
Refresh token flow
Frontend
POST /api/auth/refresh, with header Authorization: Bearer <refresh token> -> Backend
Backend
- Hash the token to fetch it from the database, don't decode it yet
- Fetch the token from the
refresh_tokentable
2.1. If token is missing from the request, send back a 400, with detail "token is missing from Authorization header"
2.2. If token is not found in the database, send back a 401, with detail "invalid"
2.3. If token is found, but revoked, send back a 401, with detail "invalid". Revoke every token the of that computing_id.
2.4. Decode the token at this point. Using the JWT decode function can also check if the token is expired or is malformed. Return 401 on these cases, with detail "invalid"
2.5. If token is found, but there is field mismatch, send back a 401, with detail "invalid". Revoke every token of that computing_id.
2.6. If the token is found, but it is expired, send back a 401, with detail "expired"
2.7. If the token is found and valid, return 200. Revoke this token and generate an access and refresh token. Set the access token onto the cookie and return the refresh token in body{ token: <new refresh token> }
Implementation Phases
To implement this, I suggest we break it up into phases. All timestamps should be in UTC time in milliseconds (UNIX timestamp) to prevent issues with timezones
Phase 1: Replacing Session ID with a JWT access token
This should be enough for a while, until we decide to create other applications that will share authentications. Once a user's session is up, we ask them to reverify themselves.
- One token, access token, which is set on the
/auth/loginresponse - Modify our cookie with the following:
- Change
session_id->access: the access JWT - Add
Max-Age: 86400 seconds (24 hours)
- Change
- Convert our session IDs into JWT tokens with the following claims
- iss: string, "https://sfucsss.org"
- sub: string, computing_id
- iat: integer, represents the UTC time of when the token was generated
- exp: integer, represents the UTC time of when the token should be expired (iat + 1 day)
- roles: array of string, contains all the privileges a user has
- could be
admin,user, etc - needs every single role and lower (e.g. an
adminshould also haveuserin its array)
- could be
- Validate requests using the JWT's information, rather than asking the database
- Continue storing session information to the
user_sessiontable - Remove the
/permissionsendpoint, since everything about the user will be stored in the JWT
Phase 2: Add authentication middleware
This should be a relatively smaller change to implement
- Add middleware that checks the cookie's JWT for every request that requires it
- Remove all the logged in and admin checks for request functions that use them
Phase 3: Add refresh tokens
This will be a huge implementation, that I hope we decide not to do
- Add refresh token and its support
- Add the refresh token table to the database
- Cookie's
Max-Age86400 seconds (1 day) -> 900 seconds (15 minutes) - Add a refresh scheme to automatically request the user to send their refresh token
- Add the
/auth/refreshendpoint - Update all frontend applications to manage sending refresh tokens