Express/Mongo-Session not recovered during OAuth 2

41 views Asked by At

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.

0

There are 0 answers