pilcrowonpaper / oslo

A collection of auth-related utilities
https://oslo.js.org
MIT License
1.06k stars 35 forks source link

Support for additional headers in OAuth token request #62

Closed noxify closed 6 months ago

noxify commented 6 months ago

Hi,

while testing the library with entra id, I got the following error while trying to get the tokens:

AADSTS9002327: Tokens issued for the 'Single-Page Application' client-type may only be redeemed via cross-origin requests. Trace ID: <traceId> Correlation ID: <correlationId> Timestamp: 2024-03-22 09:42:30Z

Would it make sense the add a additional option to the validateAuthorizationCode method to set the required headers?

My current workaround is to create my own token request.


Not sure if this is a problem with all providers, but based on what I have seen, it seems that EntraID needs the origin header in the token request if we use a "public client".

Not sure if it helps, but here the code which I have used to test it. ( used hono via npm create hono@latest and updated the src/index.ts with the code below )

// required environment variables are:
// TENANT_ID and APP_ID

import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
import { MicrosoftEntraId, generateState, generateCodeVerifier } from "arctic";

import got from "got";
import { inspect } from "util";

const app = new Hono();

const redirectUri = "http://localhost:3000/api/auth/callback";

const entraId = new MicrosoftEntraId(
  process.env.TENANT_ID,
  process.env.APP_ID,
  "",
  redirectUri
);

app.get("/", async (c) => {
  const state = generateState();
  const codeVerifier = generateCodeVerifier();

  const url = await entraId.createAuthorizationURL(state, codeVerifier, {
    scopes: ["openid", "profile", "email", `${process.env.APP_ID}/.default`],
  });

  // store state verifier as cookie
  setCookie(c, "state", state, {
    secure: false,
    path: "/",
    httpOnly: true,
    maxAge: 60 * 10, // 10 min
  });

  // store code verifier as cookie
  setCookie(c, "code_verifier", codeVerifier, {
    secure: false,
    path: "/",
    httpOnly: true,
    maxAge: 60 * 10, // 10 min
  });
  return c.html(`
    <div>
      <a href="${url}">Get access token</a>
    </div>
  `);
});

app.get("/api/auth/callback", async (c) => {
  const code = c.req.query("code");
  const state = c.req.query("state");

  const storedState = getCookie(c, "state");
  const storedCodeVerifier = getCookie(c, "code_verifier");

  if (!code || !storedState || !storedCodeVerifier || state !== storedState) {
    // 400
    throw new Error("Invalid request");
  }

  /**
   * since we can't (yet) set the origin for the token request
   * we have to use our own implementation to get the tokens
   * implementation is based on https://github.com/pilcrowOnPaper/oslo/blob/main/src/oauth2/index.ts
   */
  // const tokens = await entraId.validateAuthorizationCode(
  //   code,
  //   storedCodeVerifier
  // );

  const tokenResponse = await got(
    `https://login.microsoftonline.com/${env.TENANT_ID}/oauth2/v2.0/token`,
    {
      method: "post",
      form: {
        code: code,
        client_id: process.env.APP_ID,
        grant_type: "authorization_code",
        redirect_uri: redirectUri,
        code_verifier: storedCodeVerifier,
      },
      responseType: "json",
      headers: {
        origin: redirectUri,
      },
      throwHttpErrors: false,
    }
  );

  deleteCookie(c, "code");
  deleteCookie(c, "state");

  if (!tokenResponse.ok) {
    console.log({ error: tokenResponse.body });
    return c.text(
      "Something went wrong while fetching the tokens ( check the console for more details )"
    );
  }

  return c.text(inspect(tokenResponse.body, { depth: 2 }));
});

const port = 3000;
console.log(`Server is running on port ${port}`);

serve({
  fetch: app.fetch,
  port,
});
pilcrowonpaper commented 6 months ago

Is it possible to use confidential client instead?

noxify commented 6 months ago

thanks for the quick reply.

Is it possible to use confidential client instead?

Have to check this - thanks for the hint.

noxify commented 6 months ago

Checked it - Unfortunately I can't use the confidential client in this case, because the shown code is not hosted on a server - it's running locally. ( currently working on a POC at work which uses lucia, here I can use the confidential client and I do not have the problem )

what's the usecase for this:

We have an api which supports the authorization code flow and client credentials flow. We mentioned that postman disabled the option to set the headers for the token request if you're not signed in.

Based on this, we created a this script to give the user the option to generate the access token which can be used later in the api request.

While writing the usecase, I realized that this could be an edge case and using an other api client to test the api locally via the authorization code flow could solve the problem 🙈

Feel free to close this issue, if you do not plan to support headers for the authorization code flow.

pilcrowonpaper commented 6 months ago

Makes sense. All the providers here are mostly for the server. I guess this is expected behavior but feel free to create a new feature request for an Entra Provider for public clients