jackson icon indicating copy to clipboard operation
jackson copied to clipboard

Jackson NPM library cannot correctly POST to Azure ADFS via next-auth

Open SacredMesa opened this issue 2 years ago • 13 comments

Issue Summary

In attempting to sign-in with Azure ADFS, the following error comes up:

AADSTS750052: SAMLRequest or SAMLResponse must be present in body of HTTP request for SAML POST binding

It appears that jackson only sends the SAMLRequest through the query params, however this needs to be sent in the body. I've attempted to inject this manually into the body at the auth/saml/authorise endpoint, but was unsuccessful in getting it in. I know I'm correctly hitting my Azure as it only reaches this error when using our company's VPN. Outside the network Azure reaches the non-allowed environment screen.

I'm using the Jackson NPM library through a next-auth custom provider in a Next.js app. Followed the steps as described in the docs here: https://boxyhq.com/guides/jackson/frameworks/nextjs#setup-saml-jackson

Steps to Reproduce

  1. Follow the docs at https://boxyhq.com/guides/jackson/frameworks/nextjs#setup-saml-jackson
  2. Attempt to sign in to Azure ADFS

Technical details

  • @boxyhq/saml-jackson version: 1.9.6
  • Node.js version: 16.13.1
  • Next.js version: 12.3.1
  • Next-auth version: 4.22.1

SacredMesa avatar Aug 07 '23 09:08 SacredMesa

Thanks for reporting this @SacredMesa, we'll look into the issue.

deepakprabhakara avatar Aug 07 '23 12:08 deepakprabhakara

@SacredMesa The authorize call should return an authorize_form value that is basically the POST binding response. You can send that back as the response inside the authorize endpoint. See our service implementation here: https://github.com/boxyhq/jackson/blob/a6883734e59154a6515701322699ad6e2abbacdd/pages/api/oauth/authorize.ts#L22

niwsa avatar Aug 07 '23 18:08 niwsa

Thanks for the quick reply @deepakprabhakara and @niwsa. I've tried the implementation you linked, but I am still facing the same error.

From my understanding, the issue comes when Jackson sends a POST request to Azure's login url. The SAMLRequest is sent in the query params, however Azure is expecting this to be in the body, and so returns a 400 and the error screen.

Screenshots of the issue:

POST to Azure login url returns 400: image001

Payload of POST request to Azure login url (SAMLRequest in query params and absent in body): image002

SacredMesa avatar Aug 08 '23 05:08 SacredMesa

@SacredMesa Jackson would use the redirect binding if the IdP metadata has one, else it uses the POST binding. Can you check your ADFS metadata for the supported bindings?

niwsa avatar Aug 08 '23 06:08 niwsa

@niwsa It appears my metadata shows both bindings:

image

SacredMesa avatar Aug 08 '23 06:08 SacredMesa

~~If the metadata has a redirect binding then the IdP should support the same. But looks like that is not the case here.~~

@SacredMesa From the screenshot you shared earlier, it looks like the app is making a POST instead of a GET. Jackson simply returns the redirect_url or the post binding in this case (does not make a request).

niwsa avatar Aug 08 '23 06:08 niwsa

@SacredMesa Would you mind posting the code snippet, we can take a look to see if we can spot anything.

deepakprabhakara avatar Aug 08 '23 12:08 deepakprabhakara

@niwsa @deepakprabhakara Really appreciate the help so far! I managed to get past the error by removing the redirect binding in the metadata xml file. A bit hacky, but it works. It's properly adding the authorize_form now. Though, not sure why it was making a POST request instead of a GET for the redirect binding. When I logged the incoming req.method in authorize.ts, it was a GET request.

The issue I'm facing now is that I'm getting a timeout error after getting the redirect_url from ACS:

redirect_url in ACS https://<app-base-url>/api/auth/callback/jackson-saml?code=9ac638b03b03a87a61cc35bd5bc41d5e286e8a51&state=lMq9j_GpbQENA-2ZNPadcfUo7eKSrbw4cd4AGCkNFh8
--
[next-auth][error][OAUTH_CALLBACK_ERROR]
https://next-auth.js.org/errors#oauth_callback_error outgoing request timed out after 3500ms {
error: RPError: outgoing request timed out after 3500ms
at /app/node_modules/next-auth/node_modules/openid-client/lib/helpers/request.js:137:13
at async Client.grant (/app/node_modules/next-auth/node_modules/openid-client/lib/client.js:1316:22)
at async Client.oauthCallback (/app/node_modules/next-auth/node_modules/openid-client/lib/client.js:603:24)
at async oAuthCallback (/app/node_modules/next-auth/core/lib/oauth/callback.js:111:16)
at async Object.callback (/app/node_modules/next-auth/core/routes/callback.js:52:11)
at async AuthHandler (/app/node_modules/next-auth/core/index.js:208:28)
at async NextAuthApiHandler (/app/node_modules/next-auth/next/index.js:22:19)
at async NextAuth._args$ (/app/node_modules/next-auth/next/index.js:106:14)
at async Object.apiResolver (/app/node_modules/next/dist/server/api-utils/node.js:366:9)
at async NextNodeServer.runApi (/app/node_modules/next/dist/server/next-server.js:481:9) {
name: 'OAuthCallbackError',
code: undefined
},
providerId: 'jackson-saml',
message: 'outgoing request timed out after 3500ms'
}

Although, haven't debugged far enough to tell if it's an error with jackson, next-auth, or something else entirely. Would still be really grateful if you could take a look at the endpoints and config for jackson though if there might be something wrong/missing.

I'll share the jackson.ts, authorize.ts, acs.ts, token.ts, userinfo.ts and [...nextauth].ts codes here:

lib/jackson.ts

import type { JacksonOption, SAMLJackson } from '@boxyhq/saml-jackson';
import jackson from '@boxyhq/saml-jackson';

const samlAudience = process.env.AZURE_SAML_APPID;
const samlPath = '/api/auth/saml/acs';

const preLoadedConnection = 'lib/connections';

const opts: JacksonOption = {
  externalUrl: `${process.env.NEXTAUTH_URL}`,
  samlAudience,
  samlPath,
  preLoadedConnection,
  db: { engine: 'mem' }
};

const g = global as any;

export default async function init() {
  if (!g.jacksonInstance) {
    g.jacksonInstance = await jackson(opts);
  }

  return g.jacksonInstance as SAMLJackson;
}

pages/api/auth/saml/authorize.ts

import { NextApiRequest, NextApiResponse } from 'next';

import jackson from '../../../../lib/jackson';
import { OAuthReq } from '@boxyhq/saml-jackson';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    if (req.method !== 'GET' && req.method !== 'POST') {
      throw { message: 'Method not allowed', statusCode: 405 };
    }

    const { oauthController } = await jackson();
    const requestParams = req.method === 'GET' ? req.query : req.body;

    const { redirect_url, authorize_form } = await oauthController.authorize(
      requestParams as unknown as OAuthReq
    );

    if (redirect_url) {
      res.redirect(302, redirect_url);
    } else {
      res.setHeader('Content-Type', 'text/html; charset=utf-8');
      res.send(authorize_form);
    }
  } catch (err: any) {
    console.error('authorize error:', err);
    res.redirect('/');
  }
}

pages/api/auth/saml/acs.ts

import type { NextApiRequest, NextApiResponse } from 'next';

import jackson from '../../../../lib/jackson';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { oauthController } = await jackson();
  const { RelayState, SAMLResponse } = req.body;

  const { redirect_url } = await oauthController.samlResponse({
    RelayState,
    SAMLResponse
  });

  console.log('redirect_url in ACS', redirect_url);

  return res.redirect(302, redirect_url as string);
}

pages/api/auth/saml/token.ts

import type { NextApiRequest, NextApiResponse } from 'next';

import jackson from '../../../../lib/jackson';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { oauthController } = await jackson();
  const response = await oauthController.token(req.body);

  return res.json(response);
}

pages/api/auth/saml/userinfo.ts

import type { NextApiRequest, NextApiResponse } from 'next';

import jackson from '../../../../lib/jackson';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { oauthController } = await jackson();

  const authHeader = req.headers['authorization'];

  if (!authHeader) {
    throw new Error('Unauthorized');
  }

  const token = authHeader.split(' ')[1];
  const user = await oauthController.userInfo(token);

  return res.json(user);
}

provider in pages/api/auth/[...nextauth].ts

    {
      id: 'jackson-saml',
      name: 'Azure-SAML',
      type: 'oauth',
      checks: ['pkce', 'state'],
      authorization: {
        url: `${process.env.NEXTAUTH_URL}/api/auth/saml/authorize`,
        params: {
          scope: '',
          response_type: 'code',
          provider: 'saml'
        }
      },
      token: {
        url: `${process.env.NEXTAUTH_URL}/api/auth/saml/token`,
        params: { grant_type: 'authorization_code' }
      },
      userinfo: `${process.env.NEXTAUTH_URL}/api/auth/saml/userinfo`,
      profile: (profile) => {
        return {
          id: profile.id || '',
          firstName: profile.firstName || '',
          lastName: profile.lastName || '',
          email: profile.email || '',
          name: `${profile.firstName || ''} ${profile.lastName || ''}`.trim(),
          email_verified: true
        };
      },
      options: {
        clientId: `tenant=${process.env.AZURE_SAML_TENANT}&product=${process.env.AZURE_SAML_PRODUCT}`,
        clientSecret: 'dummy'
      }
    },

SacredMesa avatar Aug 08 '23 13:08 SacredMesa

Could you try wrapping the token route with try catch ... and check for any errors in the token handler ? It could be that the token handler threw an error which is not handled, hence the timeout.

import type { NextApiRequest, NextApiResponse } from 'next';

import jackson from '../../../../lib/jackson';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
 try { 
  const { oauthController } = await jackson();
  const response = await oauthController.token(req.body);

  res.json(response);
  } catch (err) {
  console.error('token error:', err);
  }
}

Also noticed that you are using the mem db engine. Are you running the app locally or is it deployed somewhere? Do note that the mem db won't work in a serverless environment.

niwsa avatar Aug 08 '23 19:08 niwsa

@niwsa I've wrapped the token and acs routes with try catch, but seems there's no error being thrown there. From what it seems, is that it hangs as it's trying to redirect. I can't tell if it's actually making the redirect request tbh, though the redirect url is there.

The app is deployed in AWS ECS. If I'm not mistaken the db is to store connection configs? I've set it up to preload the connection from a .js and .xml file though, would this suffice?

SacredMesa avatar Aug 16 '23 03:08 SacredMesa

If I'm not mistaken the db is to store connection configs?

We also use it to conduct the Auth flow, as the OAuth 2.0 entities like code and token need to be persisted for some time.

Could you try increasing the HTTP timeout option inside the NextAuth Provider?

 httpOptions: {
        timeout: 30000,
},

niwsa avatar Aug 17 '23 09:08 niwsa

@niwsa I've increased the timeout option, but doesn't seem to make a difference. We've had to drop this for awhile to handle some other tickets, but back on it now.

The last loggable event is still from the acs endpoint. We're getting the redirect url (https://<app-base-url>/api/auth/callback/jackson-saml?code=9ac638b03b03a87a61cc35bd5bc41d5e286e8a51&state=lMq9j_GpbQENA-2ZNPadcfUo7eKSrbw4cd4AGCkNFh8), but hanging after that.

Do you have any other suggestions on what we could try?

SacredMesa avatar Aug 29 '23 10:08 SacredMesa

@SacredMesa We can possibly connect over a call and sort out the issue if that's OK for you. You can ping me in the discord server: https://discord.com/invite/uyb7pYt4Pa.

niwsa avatar Aug 29 '23 12:08 niwsa

@SacredMesa Closing this, please re-open if this continues to be an issue.

deepakprabhakara avatar Mar 02 '24 16:03 deepakprabhakara