angularcookiesopenid-connectsamesitenode-oidc-provider

Angular SPA which is used as Frontend for my custom OIDC provider is not sending session cookie to my backend /signin API


Senario

I have a two angular Apps first one is a angular_shop an which use /auth endpoint for a custom node_oidc_provider to start auth request

node_oidc_provider the checks the PKCE code from angular_shop and redirects to second angular app angular_oidc_frontend which acts as a frontend of custom identity provider created using node_oidc_provider

I can see that the redirection to http:localhost:4200/signin?uid=WIkb3oodGHMOTgg4mlkyQ is happening successfully

But now i have two problems

  1. the session cookie set by node_oidc_provider is not visible in application tab of the chrome in first load of angular application, but it can be seen if i refresh the angular application page.

enter image description here enter image description here

  1. Second problem is that even though the cookie sameSite is set to None the two cookie visible is not send to the server when i submit the login details to localhost:8081/api/v1/sigin endpoind to the idp sever which is created using node_oidc_provider

Here is the request and response header to /signin endpoint which i logged at server

    Received headers: {
  'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8',
  'accept-encoding': 'gzip, deflate, br',
  referer: 'http://localhost:4200/signin?uid=PbR6zZdrqGsDauDy-Sb1X',
  'sec-fetch-dest': 'empty',
  'sec-fetch-mode': 'cors',
  'sec-fetch-site': 'same-origin',
  origin: 'http://localhost:4200',
  'sec-ch-ua-platform': '"macOS"',
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
  'sec-ch-ua-mobile': '?0',
  'content-type': 'application/json',
  accept: 'application/json, text/plain, */*',
  'sec-ch-ua': '"Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"',
  'content-length': '116',
  connection: 'close',
  host: 'localhost:8081'
}
Sent headers: [Object: null prototype] {
  'x-powered-by': 'Express',
  'access-control-allow-origin': 'http://localhost:4200',
  'access-control-allow-methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
  'access-control-allow-headers': 'X-Requested-With,content-type,Authorization,skip',
  'access-control-allow-credentials': 'true'
}

Note: initially i though this must be due to two different origin 'http://localhost:4200andhttp://localhost:8081` so setup a proxy at angular cli with following setup

{
    "/temp": {
      "target": "http://localhost:8081",
      "secure": false,
      "pathRewrite": {
        "^/temp": ""
      },
      "logLevel": "debug",
      "changeOrigin":true,
      "cookieDomainRewrite":"localhost"
    }
  }

Also the angular app is also configured to send the withCredentials: true for /signin endpoint.

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
  withCredentials: true 
};

Other configuration at the backend is as following

oidc-provide cookie-config:

cookies: {
    keys: ['ewdewdewdewdewdwd', 'dewdewdwedwedwedewd', 'dwedwedwdwd'],
    long: {
        signed: true,
        httpOnly: true,
        maxAge: 86400 * 1000, // 1 day in milliseconds
        // Add secure flag in production
        secure: process.env.NODE_ENV === 'production',
        // Add sameSite policy (strict, lax, or none)
        sameSite: 'none',
    },
    short: {
        signed: true,
        httpOnly: true,
        // Add secure flag in production
        secure: process.env.NODE_ENV === 'production',
        // Add sameSite policy (strict, lax, or none)
        sameSite: 'none',
    },
    names: {
        session: '_session',
        interaction: '_interaction',
        resume: '_resume',
        state: '_state',
    },
   
}, 

custom interaction policy created

const interactions = policy();
interactions.add(new Prompt({
    name: 'signin',
    requestable: true,
    check: async (ctx) => {
        // Here, you check if the user is already authenticated
        // If not, you'll prompt for login
        return !ctx.oidc.session || !ctx.oidc.session.accountId;
    },
}), 0);

created policy modified to redirect to correct url

 interactions: {
        policy: interactions,
        url(ctx, interaction) {console.info('interaction',interaction)
            return `${process.env.IDENTITY_FRONTEND_URL}/signin?uid=${interaction.uid}`;
        },
    },

And here is my CROS config at the server

app.use((req, res, next) => {
    // Website you wish to allow to connect    
    res.setHeader('Access-Control-Allow-Origin', 'http://localhost:4200');

    // Request method you wish to allow
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');

    // Request headers you wish to allow 
    res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type,Authorization,skip');

    // Set to true if you need the website to include cookies in the request sent 
    // to the API (e.g. in case you use sessions)
    res.setHeader('Access-Control-Allow-Credentials', 'true');

    // Pass to the next layer of middleware
    next();
});

And my /signin endpoint

signIn: async (req, res, next) => {

    try {
        if (!req.isAuthenticated()) {
            return res.status(401).send('Authentication failed');
        }

        const interactionId = req.body.uid || req.query.uid || req.params.uid ;

        if (!interactionId) {
            return res.status(400).send('Interaction token is missing');
        }
        //console.info('req incoming',req);
        const details = await oidcProvider.interactionDetails(req, res);
        console.log(details);
        assert.equal(details.interaction.uid, interactionId);

        const { prompt: { name }, params, session } = details;

        if (name === 'signin') {
            // Assuming you have a method or a way to get accountId from req.user
            const accountId = req.user._id.toString(); // Use a unique identifier for the user
            
            const result = {
                login: { accountId },
            };

            await oidcProvider.interactionFinished(req, res, result, { mergeWithLastSubmission: false });
        } else {
            res.status(400).send('Unsupported interaction type');
        }
    } catch (error) {
        console.error(error);
        res.status(500).send('An error occurred during authentication');
    }
},

And for visibility here the error at server due to no cookie being detected

{
  allow_redirect: true,
  error: 'invalid_request',
  status: 400,
  statusCode: 400,
  expose: true,
  error_description: 'interaction session id cookie not found',
  error_detail: undefined
}

Also in production i plan to use nginx to reverse proxy angular_oidc_frontend app and ipd nodejs api to serve from same domain to resolve the sameSite cookie problem.

Advanced sorry for the long post, its a bit tricky to make short explanation for this complex flow between the apps, what i am implementing is ( Authorisation code flow ) oidc in custom idp (identity service provider) server created using node_oidc_provider


Solution

  • According to the 2dn "dev tools" screenshot, the cookie path is /signin, meaning that it will only be sent with requests starting with localhost:4200/signin

    Cookie path

    But login details are sent to localhost:4200/api/v1/signin instead so the cookie will be ignored

    Changing the cookie path to / should solve the problem.