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/login endpoint
- 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
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
admin should also have user in its array)
- Created after a successful verification with SFU CAS
- Stored in the cookie under the key
session_id
- Utilize
httpOnly and secure (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 401 with WWW-Authenticate=Bearer
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-Agent header 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/refresh with the header Authorization: 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_token table
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/login response
- Modify our cookie with the following:
- Change
session_id -> access: the access JWT
- Add
Max-Age: 86400 seconds (24 hours)
- 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
admin should also have user in its array)
- Validate requests using the JWT's information, rather than asking the database
- Continue storing session information to the
user_session table
- Remove the
/permissions endpoint, 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-Age 86400 seconds (1 day) -> 900 seconds (15 minutes)
- Add a refresh scheme to automatically request the user to send their refresh token
- Add the
/auth/refresh endpoint
- Update all frontend applications to manage sending refresh tokens
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_tokentable.Backend
/auth/loginendpoint/auth/refresh, which takes the refresh token in the body and verifies it with the backendDatabase
Modify the
user_sessiontablesession_id->access_token: holds the access token's JWT valueMake 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 agentAccess token
admin,user, etcadminshould also haveuserin its array)session_idhttpOnlyandsecure(already done)HTTP 401withWWW-Authenticate=BearerRefresh token
computing_idUser-Agentheader of the request when the refresh token was generatedrefresh_tokenFrontend
/api/auth/refreshwith the headerAuthorization: Bearer <refresh_token>Login flow
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
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 headerAuthorization: Bearer <refresh token>-> BackendBackend
refresh_tokentable2.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.
/auth/loginresponsesession_id->access: the access JWTMax-Age: 86400 seconds (24 hours)admin,user, etcadminshould also haveuserin its array)user_sessiontable/permissionsendpoint, since everything about the user will be stored in the JWTPhase 2: Add authentication middleware
This should be a relatively smaller change to implement
Phase 3: Add refresh tokens
This will be a huge implementation, that I hope we decide not to do
Max-Age86400 seconds (1 day) -> 900 seconds (15 minutes)/auth/refreshendpoint