During my OAuth 2 flow I store tokens in my session, then save it to my mongoStore before redirecting to the authorize endpoint. At /callback the session is not recovered and my tokens cannot be retrieved.
I juggled the
- position of app.use(session(…)) declaration inside server.js
- session.cookie settings as well as secret, resave and saveUnitil…
- position and secret of cookie-parser
Also, I
- added req.session.save() to my /launch (=init) and ensure to call res.redirect after the session was saved
- ensured MongoConn is working properly and does store all sessions correctly (but does not retrieve them apparently)
- tried multiple browsers and reset data and allow third-party cookies
I have an https connection, logs show all variables are properly set and the flow generally does work. I receive the state-mismatch on every attempt.
For some reason the cookie connect.sid is only sent as a response cookie of /launch or /callback resp. if session.cookie.secure=false even though https. However, the cookie won't be set because sameSite is assumed 'lax'. If I set sameSite to 'none' obviously secure is required – and again, no cookie will be set. sameSite='strict' won't allow the cookie to be set because the request comes from a "cross-site response which was not the response to a top-level navigation".
In neither scenario the session is recovered.
Cookie: connect.sid s%3AecbizTJPq1IQJHooOns2w6am4QmL07RQ.HWr%2BMn8z%2B%2FA9Q00RMzeHtAANaDDMKAYHZOz2k%2Fp3XbI sub.domain.com / Session 99 ✓ Medium
server.js
const cookieParser = require('cookie-parser');
const session = require("express-session");
const MongoStore = require('connect-mongo');
const app = express();
app.set('trust proxy', 1);
app.use(cors({
credentials: true,
origin: ['https://sub.domain.com','https://sandbox.domain.com']
}));
app.use(session({
// secret: process.env.SESSION_SECRET,
secret: "deltawolf_eins11",
store: MongoStore.create({ mongoUrl: MONGODB_URI }),
resave: false,
saveUninitialized: true,
cookie: { secure: true } // or false or any other setting – does not make a difference
}));
app.use(cookieParser(process.env.SESSION_SECRET));
oauth.js
const app = express();
const router = express.Router();
// Reviewed Launch Route
router.get('/launch', async (req, res, next) => {
const { iss, launch } = req.query;
try {
const config = getConfig;
const authorizeEndpoint = config.authorizeUrl;
const state = crypto.randomBytes(16).toString('hex');
// for native mobile support create code pair
const {codeChallenge, codeVerifier} = generateCodeChallenge();
req.session.codeVerifier = codeVerifier;
req.session.tokenUrl = config.tokenUrl;
req.session.state = state;
// Prepare the parameters
const params = {
…
};
…
req.session.save((err) => {
if(err) {
// If there's an error during session saving, pass it to the error handler.
return next(new ErrorHandler(500, 'Internal Server Error while saving session.'));
}
// If session save was successful, then prepare for the redirect.
const url = `${authorizeEndpoint}?${qs.stringify(params)}`;
console.log("Session ID: ", req.sessionID); // returns a sessionID, e.g. 123
res.redirect(url);
});
} catch (error) {
next(new ErrorHandler(500, 'Error during authorization: ' + error.message));
}
});
router.get("/callback", async (req, res, next) => {
const { code, state} = req.query;
console.log("state: ",state) // Returns the previous state
console.log("Session ID: ",req.sessionID) // Returns a new different session ID, e.g. 456
console.log("State match: \n",state===req.session.state) // false
if (!code) {
return next(new ErrorHandler(400, 'Authorization code not provided')); // is triggered
}
if(state !== req.session.state) {
return next(new ErrorHandler(400, 'State parameter mismatch'));
}
const tokenUrl = req.session.tokenUrl;
const codeVerifier = req.session.codeVerifier;
// generate a base64 encoded Basic header for symmetric Client Auth
const base64EncodedClientHash = Buffer.from(CLIENT_SECRET).toString('base64');
// generate a jwt for asymmetric Client Auth
// const jwtToken = generateJWT(CLIENT_ID, tokenUrl);
const formData = {
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
code_verifier: codeVerifier
};
// Attach assertions for asymmetric Client Auth
// formData.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
// formData.client_assertion = jwtToken;
console.log(formData)
try {
// POST request to tokenUrl using symmetric Client Auth with client secret
// Content-Type must be form-urlencoded in the request body
const postResponse = await axios.post(tokenUrl, qs.stringify(formData), {
headers: {
'Authorization': 'Basic ' + base64EncodedClientHash,
'Content-Type': 'application/x-www-form-urlencoded'
}
});
req.session.tokenResponse = postResponse.data;
req.session.save(err => {
if (err) throw new ErrorHandler(500, 'Error saving session data.');
res.redirect('https://sub.domain.com/');
});
} catch (error) {
next(new ErrorHandler(500, 'Error during the token exchange POST request.'));
}
});
The network request for /launch looks like:
// Response Header:
Access-Control-Allow-Credentials: true
Connection: keep-alive
Content-Length: 1724
Content-Type: text/html; charset=utf-8
Date: Thu, 07 Sep 2023 09:38:54 GMT
Location: https://sandbox.domain.com/v/r4/auth/authorize?…%20launch&response_type=code&client_id=2f**1&redirect_uri=https%3A%2F%2Fsub.domain.com%2Fapi%2Foauth%2Fcallback&state=b***d2&code_challenge=jk**7k&code_challenge_method=S256&launch=Wz**DFd…
Server: nginx
Set-Cookie: connect.sid=s%3**E8c; Path=/; HttpOnly; SameSite=Strict
Vary: Origin, Accept
X-Powered-By: Express
// Request Header:
GET /api/oauth/launch?iss=…&launch=… HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: de-DE,de;q=0.9,en;q=0.8,en-US;q=0.7
Cache-Control: no-cache
Connection: keep-alive
DNT: 1
Host: sub.domain.com
Pragma: no-cache
Referer: https://sandbox.domain.com/
Sec-Fetch-Dest: iframe
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36
sec-ch-ua: "Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
I can't find any fitting solution.