Oauth2 + open id connect working locally but not on netlify website

I am working on a login flow:

I have a website with Vipps login(Norwegian provider for login service) at this address: https://preprod-community.netlify.app/login/

The problem:

I do two request, one to the login provider for authorizing, and another to an external api which in turn uses my data for getting an accesToken.

Everything works locally but on the netlify site i get the error: The PKCE code challenge did not match the code verifier.

What happens is that the first request sends a codeChallenge based on a code Verifier, the second requests should use the same code Verifier but i looks like a new codeChallenge and code Verifier gets generated in the auth-callback.js file.

On localhost both requests uses one codeChallenge and code Verifier and everything works fine.

logging in auth.js:

codeVerifier in oauth DvP_gsSpU2SDfIsUaLXdDNT5MvmbwO790-Ll1KDehg8
codeChallenge in oauth y_ptHuBqjWbEGfaOwpF_z_9TJo_kJ-Yqew1xAPJ-424
config from oauth {
  client: {
    id: 'some-id',
    secret: 'some-secret'
  },
  codeVerifier: 'DvP_gsSpU2SDfIsUaLXdDNT5MvmbwO790-Ll1KDehg8',
  codeChallenge: 'y_ptHuBqjWbEGfaOwpF_z_9TJo_kJ-Yqew1xAPJ-424',
  stateUuid: '97028e63-b02d-444e-bdb8-8b1a31b33e8b',
  siteUrl: 'https://preprod-community.netlify.app',
  redirectUri: 'https://preprod-community.netlify.app/.netlify/functions/auth-callback'
}

logging auth-callback.js:

codeVerifier in oauth ogHvWKuscekm0KTCwBtTvLkMbTgX2O0n4Ood67P_bzA
codeChallenge in oauth CWe3GHSYaU9TPnlK0LCoakbB0aIhSZ5OuX7r--diruc
config from callback {
  client: {
    id: 'some-id',
    secret: 'some-secret'
  },
  codeVerifier: 'ogHvWKuscekm0KTCwBtTvLkMbTgX2O0n4Ood67P_bzA',
  codeChallenge: 'CWe3GHSYaU9TPnlK0LCoakbB0aIhSZ5OuX7r--diruc',
  stateUuid: '36b74e9b-b100-4de1-9bff-483edf24c7ce',
  siteUrl: 'https://preprod-community.netlify.app',
  redirectUri: 'https://preprod-community.netlify.app/.netlify/functions/auth-callback'
}

Files in use:

oauth.js

const { v4 } = require('uuid');
const { AuthorizationCode } = require('simple-oauth2');

const crypto = require('crypto');

const vippsApi = 'https://api.vipps.no/access-management-1.0/access';
const siteUrl = process.env.URL || 'http://localhost:8888';
const redirectUri = `${siteUrl}/.netlify/functions/auth-callback`;

const stateUuid = v4();

function base64URLEncode(str) {
  return str
    .toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

const codeVerifier = base64URLEncode(crypto.randomBytes(32));

console.log('codeVerifier in oauth', codeVerifier);

function sha256(buffer) {
  return crypto.createHash('sha256').update(buffer).digest();
}

const codeChallenge = base64URLEncode(sha256(codeVerifier));
console.log('codeChallenge in oauth', codeChallenge);

const config = {
  client: {
    id: process.env.VIPPS_CLIENT_ID,
    secret: process.env.VIPPS_CLIENT_SECRET,
  },
  codeVerifier,
  codeChallenge,
  stateUuid,
  siteUrl,
  redirectUri,
};

const oauth2 = new AuthorizationCode({
  client: {
    id: config.client.id,
    secret: config.client.secret,
  },
  auth: {
    tokenHost: vippsApi,
    authorizePath: `${vippsApi}/oauth2/auth`,
    authorizeHost: vippsApi,
  },
});

module.exports = {
  oauth2,
  config,
  siteUrl,
};

auth.js

const { config, oauth2 } = require('./oauth');

// /* Do initial auth redirect */
exports.handler = (event, context, callback) => {
  console.log('config from oauth', config);
  const authorizationUri = oauth2.authorizeURL({
    scope: 'openid api_version_2 address birthDate name phoneNumber',
    state: config.stateUuid,
    redirect_uri: config.redirectUri,
    response_type: 'code',
    code_challenge_method: 'S256',
    code_challenge: config.codeChallenge,
  });

  /* Redirect user to authorizationURI */
  const response = {
    statusCode: 302,
    headers: {
      Location: authorizationUri,
      'Cache-Control': 'no-cache',
    },
    body: '', // return body for local dev
  };

  return callback(null, response);
};

auth-callback.js

const axios = require('axios');
const cookie = require('cookie');
const { config } = require('./oauth');

console.log('config from callback', config);

exports.handler = async (event) => {
  const { code } = event.queryStringParameters;
  /* state helps mitigate CSRF attacks & Restore the previous state of your app */
  const { state } = event.queryStringParameters;

  const API_ENDPOINT = `https://communityas-api.azurewebsites.net/api/Account/authorize?response_type=code&state=${state}&code=${code}&codeVerifier=${config.codeVerifier}&redirectUri=${config.redirectUri}`;

  let response;

  const hour = 3600000;
  const twoWeeks = 14 * 24 * hour;

  const redirectUrl = `${config.siteUrl}/login`;
  // Do redirects via html
  const html = `
  <html lang="en">
    <head>
      <meta charset="utf-8">
    </head>
    <body>
      <noscript>
        <meta http-equiv="refresh" content="0; url=${redirectUrl}" />
      </noscript>
    </body>
    <script>
      setTimeout(function() {
        window.location.href = ${JSON.stringify(redirectUrl)}
      }, 0)
    </script>
  </html>`;

  try {
    response = await axios.post(API_ENDPOINT);
  } catch (err) {
    return {
      statusCode: err.statusCode || 500,
      body: html,
    };
  }

  const accessToken = cookie.serialize(
    'accessToken',
    response.data.accessToken,
    {
      secure: true,
      httpOnly: false,
      path: '/',
      maxAge: twoWeeks,
    }
  );

  const id = cookie.serialize('id', response.data.id, {
    secure: true,
    httpOnly: false,
    path: '/',
    maxAge: twoWeeks,
  });

  const firstName = cookie.serialize('firstName', response.data.firstName, {
    secure: true,
    httpOnly: false,
    path: '/',
    maxAge: twoWeeks,
  });

  const refreshToken = cookie.serialize(
    'refreshToken',
    response.data.refreshToken,
    {
      secure: true,
      httpOnly: false,
      path: '/',
      maxAge: twoWeeks,
    }
  );

  const userCookies = [accessToken, id, firstName, refreshToken];

  return {
    statusCode: 200,
    headers: {
      'Set-Cookie': userCookies,
      'Cache-Control': 'no-cache',
      'Content-Type': 'text/html',
    },
    body: html,
  };
};

Hi @henrikfischer,

Sorry to take longer on this, but I don’t think anyone here is experienced with this kind of a setup. Maybe someone else is already doing this on Netlify, but they probably are not using the forums.

Anyways, simply by looking at the code, it’s hard to guess what’s going wrong. We could try to debug this together, but would it be possible to share some reproduction steps and possibly share a repo?