We develop a website in JAMStack, all URLs are static HTML page, and each interaction with the server are made by a fetch call on our REST API (micro-services).
When a user sign in, we want to proposes an option to permit them to stay connected as long as they want (years if neeeded). This increase the risks of interception of this session id, so we want to mitigate some attacks (hijacking, session replay, session fixation, tampering, CSRF ... )
My idea is to keep the session id in a cookie (httpOnly, sameSite, secure, path=/api/), and after sign-in to share a random secret to save in a non-extractable CryptoKey that will sign API requests like this:
crypto.sign('HMAC', CryptoKey, timestamp + API_REQUEST_URL + API_REQUEST_JSON)
I put a scenario to explain this in details. Each micro-services share a SERVER_SECRET, a random value of 128 bits
Sign-in:
Client
| Send JSON { email, password }
V
Server
| Verify user credentials
| Generate a random SESSION_ID of 128 bits
| Generate a CLIENT_SECRET = sha1(SERVER_SECRET + SESSION_ID)
| Store in DB sessions[SESSION_ID] the identity { user, authorizations, ... }
| Add in the response header the cookie containing SESSSION_ID
| Return JSON { CLIENT_SECRET, timestamp }
V
Client
- Save CLIENT_SECRET in a non-extractable CryptoKey
- Save in localStorage DELTA_SYNC, the delta in seconds from the server timestamp and the client
Next API requests:
Client
| Calcul the current TIME_STEP (current timestamp divided per 5 seconds + DELTA_SYNC)
| Uses the CryptoKey to sign (TIME_STEP + URL + JSON)
| Send the JSON + header x-signature
V
Server
| Calcul the current TIME_STEP
| Extract SESSION_ID from the cookie
| Generate the SECRET_CLIENT = sha1(SERVER_SECRET + SESSION_ID)
| Check authenticity crypto.verify('HMAC', SECRET_CLIENT, x-signature, TIME_STEP + URL + JSON)
| --> If not match retry with (TIME_STEP - 5 seconds)
| If matches, we check if the session is still alive in DB
| Then we got the user & his authorizations to execute the request
| (...)
| For all responses:
| - We also sign the TIME_STEP+URL+JSON and add it to the header x-signature
| - To prevent time desynchrosiation, we also add the header x-timestamp of our timestamp
| (the client can readjust the DELTA_SYNC if needed)
V
Client
| Calcul the TIME_STEP and check authenticity of the answer
Advantages:
- If a CRSF attack steal the SESSION_ID, it's useless without the CryptoKey to sign the request
- If a XSS attack access the CryptoKey to sign, it's useless without the SESSION_ID
- Prevent replay attacks (time step of 5 seconds)
- Prevent session fixation (the SESSION_ID alone isn't enough)
- Each client/server have proof that the request/response isn't tampered
- User can stay logged as long as he want
Security questions:
- Do you believe there is still some attacks possible to hijack the session of the user? (excepted of a physical access to the device of the victim)
Crypto questions:
- Is it safe to generate the
CLIENT_SECRET
fromsha1(SERVER_SECRET + SESSION_ID)
or do you have any suggestions to improve this point?