There are two applications:
auth-server
- identity provider - this is my custom SSO solution.img-server
- service provider - this application has some routes that I would like to authenticate.The auth-server
has two endpoints:
GET /
- ex. URL: https://auth.domain.com/?redirect=https://img.domain.com/getdata
redirect
query parameter including the public token as a query parameter in the redirect (authToken
).POST /
- ex. URL: https://auth.domain.com/?redirect=https://img.domain.com/getdata
redirect
query parameter including the public token as a query parameter in the redirect (authToken
).An example successful response to the example URL would be: https://img.domain.com/getdata?authToken={jwt}
where jwt
is the generated public token. The service providers can validate this token, because it is generated with a private-key of which the service provider know its public-key counterpart.
express
- To run the server.cookie-parser
- To set and read the (signed) cookies.jsonwebtoken
- To generate and verify JWT (with RSA key-pair).The flow that I'm trying to get working is the following, for this example I will assume that no cookie is set on the service provider yet:
https://img.domain.com/getdata
.Authorization
cookie is already set, or if an authToken
query parameter is set.https://auth.domain.com/?redirect=https://img.domain.com/getdata
.authToken
, e.g., https://img.domain.com/getdata?authToken={jwt}
.authToken
query parameter, validates it and stores it as an httpOnly cookie Authorization
.https://img.domain.com/getdata
.Authorization
cookie and use it to validate the user.The service provider code is the following (simplified):
server.get('/getdata', authenticate, (req, res) => { res.send("cool stuff") });
function authenticate(req, res, next) {
if (req.query.authToken && validateToken(req.query.authToken)) {
// If valid, then set it as cookie.
res.cookie("Authorization", req.query.authToken, { httpOnly: true, signed: true, sameSite: "strict" });
// Clear query parameter and redirect.
res.redirect( req.originalUrl.split("?").shift() );
return;
} else if (req.signedCookies.Authorization && validateToken(query.signedCookies.Authorization) {
next();
return;
}
// No valid token found, so redirect to identity service.
res.redirect(`auth.domain.com/?redirect=https://img.domain.com/${req.originalUrl}`)
}
function validateToken(token) {
// Does stuff to validate token.
}
The flow from the service provider to the identity provider and back goes correctly (step 1 - 4), but storing the Authorization
cookie won't work.
The authToken
query parameter is correctly retrieved and the validation of it is succesful, but once the query parameters are cleared and a redirect is done again it doesn't see the Authorization
cookie (I've also checked req.cookies
which doesn't contain it).
I've looked up the issue and seen different things about how cookies are exactly set in HTTP, but never been able to exactly wrap my head around why this isn't working. Does anybody know why? I'm also open to feedback on my authentication flow, maybe it's just not the correct way to do it.
I must add that I've only tried this while running the img-server
locally (http://localhost:3000
). The auth-server
is hosted on HTTPS.
Thanks in advance!
== EDIT ==
The cookie is apparently set, but it just isn't sent with the immediate redirect (to clear query parameters). When I go to localhost:3000/
afterwards, the session storage does contain the correct Authorization
cookie and visiting the authenticated route works correctly. How can I fix that it does include it in its immediate redirect?
This is a feature or 'SameSite=strict'. The first request coming in from an external origin will not send SameSite=strict cookies.
This behaviour is documented in the 'Lax' section on MDN:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#lax
The logic is that one CSRF vector is the ability to trigger some action simply by coming from an external site.