# How to login as user from outside Foundry without "prompt" or "dropdown"?

**URL:** <https://community.palantir.com/t/how-to-login-as-user-from-outside-foundry-without-prompt-or-dropdown/7190>\
**Category:** Ask the Community\
**Tags:** osdk\
**Created:** [September 7, 2026, 11:19am UTC](https://community.palantir.com/t/how-to-login-as-user-from-outside-foundry-without-prompt-or-dropdown/7190 "2026-09-07T11:19:28Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![VincentF](https://avatars.discourse-cdn.com/v4/letter/v/8491ac/32.png) [@VincentF](https://community.palantir.com/u/VincentF)\
**Post date:** [September 7, 2026, 11:19am UTC](https://community.palantir.com/t/how-to-login-as-user-from-outside-foundry-without-prompt-or-dropdown/7190/1 "2026-09-07T11:19:28Z")

</div>

I want a user to be able to login in Foundry, while outside of Foundry (e.g. in my own React app/portal in my company’s network).

How can this user login, without seeing it much/at all Foundry (no dropdown, popup, visible login flow, etc.)

---

<div class="post-metadata">

**Author:** ![VincentF](https://avatars.discourse-cdn.com/v4/letter/v/8491ac/32.png) [@VincentF](https://community.palantir.com/u/VincentF)\
**Post date:** [September 7, 2026, 11:19am UTC](https://community.palantir.com/t/how-to-login-as-user-from-outside-foundry-without-prompt-or-dropdown/7190/2 "2026-09-07T11:19:32Z")

</div>

## How to setup

You need a few things in order for the users to be able to login without popup/action from their side:

- The user need to be already logged-in with your IDP (e.g. have an active Entra browser session)
- The user need to be able to login to Foundry (so the Foundry-application need to be accessible for this user in your IDP config + Correct organization/group assignment configuration in Control Panel in Foundry)
- Organization-level OAuth application consent (see below) to avoid seeing the “allow” popup on each login.
- There should be one identity provider the user is being redirected to. If you have multiple identity providers, you need to pass the `_realm` parameter to the login page, so that the user doesn’t have to select manually which IDP integration to use. See below.

This screen is the OAuth application consent:

 ![image](https://canada1.discourse-cdn.com/palantir/original/2X/9/94915efd631fb6ae8f8d9be31f4adda0b4b686d9.png)

This can be “turned off” by allowing a whole organization access to a given application in Control Panel:

 ![image](https://canada1.discourse-cdn.com/palantir/original/2X/f/fc0d844eb529126fd60438ece8735db4225b714a.png)

The realm parameter can be passed to

```auto
https://foundry.example.com/multipass/api/oauth2/authorize?_realm=127b1e4a-5678-498a-1234-b20113eeeace

```

 ![image](https://canada1.discourse-cdn.com/palantir/original/2X/9/9f51067edf0a1164f1eecb108f884ef7ca15039c.png)

## How to test

To easily test the flow, you can use this below local webpage:

```auto
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Foundry OAuth PKCE + curl Tester</title>

  <style>
    body {
      box-sizing: border-box;
      max-width: 800px;
      margin: 40px auto;
      padding: 0 20px 60px;
      font-family: system-ui, sans-serif;
      line-height: 1.5;
    }

    label {
      display: block;
      margin: 16px 0 5px;
      font-weight: 600;
    }

    input {
      box-sizing: border-box;
      width: 100%;
      padding: 9px;
      font: inherit;
    }

    button {
      margin: 16px 8px 0 0;
      padding: 9px 14px;
      font: inherit;
      cursor: pointer;
    }

    pre {
      min-height: 50px;
      padding: 14px;
      overflow-wrap: anywhere;
      white-space: pre-wrap;
      background: #f4f5f7;
      border: 1px solid #d8dce1;
      border-radius: 5px;
    }

    .success {
      color: #08783e;
    }

    .error {
      color: #c23030;
    }

    .warning {
      padding: 12px;
      border-left: 4px solid #d99b20;
      background: #fff7e6;
    }
  </style>
</head>

<body>
  <h1>Foundry OAuth PKCE + curl Tester</h1>

  <p>
    This page performs OAuth 2.0 Authorization Code with PKCE and
    generates an authenticated curl command.
  </p>

  <label for="foundryUrl">Foundry URL</label>
  <input
    id="foundryUrl"
    type="url"
    value="https://REDACTED.com"
  />

  <label for="realm">Authentication realm (optional)</label>
  <input
    id="realm"
    placeholder="Realm ID from Control Panel"
  />

  <label for="clientId">OAuth client ID</label>
  <input
    id="clientId"
    placeholder="YOUR_FOUNDRY_CLIENT_ID"
    autocomplete="off"
  />

  <label for="redirectUri">Registered callback URI</label>
  <input id="redirectUri" type="url" />

  <label for="scopes">OAuth scopes</label>
  <input
    id="scopes"
    value="offline_access api:use-ontologies-read api:use-ontologies-write"
  />

  <div>
    <button id="loginButton">Log in</button>
    <button id="tokenButton">Check/refresh token</button>
    <button id="clearButton">Clear local session</button>
  </div>

  <h2>curl request</h2>

  <label for="apiUrl">Foundry API URL or path</label>
  <input
    id="apiUrl"
    value="/api/v2/ontologies"
  />

  <div>
    <button id="generateButton">Generate curl</button>
    <button id="copyButton">Copy curl</button>
  </div>

  <h2>Status</h2>
  <pre id="status">Ready.</pre>

  <h2>Generated curl</h2>

  <p class="warning">
    The generated command contains a live bearer token. Do not share it.
  </p>

  <pre id="curlOutput">No command generated.</pre>

  <script>
    const STORAGE = {
      config: "foundry.config",
      state: "foundry.state",
      verifier: "foundry.verifier",
      tokens: "foundry.tokens",
    };

    const elements = {
      foundryUrl: document.querySelector("#foundryUrl"),
      realm: document.querySelector("#realm"),
      clientId: document.querySelector("#clientId"),
      redirectUri: document.querySelector("#redirectUri"),
      scopes: document.querySelector("#scopes"),
      apiUrl: document.querySelector("#apiUrl"),
      status: document.querySelector("#status"),
      curlOutput: document.querySelector("#curlOutput"),
    };

    elements.redirectUri.value =
      window.location.origin + window.location.pathname;

    function setStatus(message, type = "") {
      elements.status.textContent = message;
      elements.status.className = type;
    }

    function getConfig() {
      const config = {
        foundryUrl:
          elements.foundryUrl.value.trim().replace(/\/+$/, ""),
        realm: elements.realm.value.trim(),
        clientId: elements.clientId.value.trim(),
        redirectUri: elements.redirectUri.value.trim(),
        scopes: elements.scopes.value.trim(),
      };

      if (
        !config.foundryUrl ||
        !config.clientId ||
        !config.redirectUri ||
        !config.scopes
      ) {
        throw new Error("Complete all required OAuth fields.");
      }

      new URL(config.foundryUrl);
      new URL(config.redirectUri);

      return config;
    }

    function saveConfig(config) {
      sessionStorage.setItem(
        STORAGE.config,
        JSON.stringify(config),
      );
    }

    function loadConfig() {
      const serialized =
        sessionStorage.getItem(STORAGE.config);

      if (!serialized) {
        return;
      }

      try {
        const config = JSON.parse(serialized);

        elements.foundryUrl.value =
          config.foundryUrl ?? elements.foundryUrl.value;

        elements.realm.value =
          config.realm ?? "";

        elements.clientId.value =
          config.clientId ?? "";

        elements.redirectUri.value =
          config.redirectUri ?? elements.redirectUri.value;

        elements.scopes.value =
          config.scopes ?? elements.scopes.value;
      } catch {
        sessionStorage.removeItem(STORAGE.config);
      }
    }

    function base64Url(bytes) {
      return btoa(String.fromCharCode(...bytes))
        .replaceAll("+", "-")
        .replaceAll("/", "_")
        .replaceAll("=", "");
    }

    function randomValue() {
      return base64Url(
        crypto.getRandomValues(new Uint8Array(32)),
      );
    }

    async function sha256(value) {
      const digest = await crypto.subtle.digest(
        "SHA-256",
        new TextEncoder().encode(value),
      );

      return new Uint8Array(digest);
    }

    function storeTokens(tokens) {
      const stored = {
        ...tokens,
        expires_at:
          Date.now() + Number(tokens.expires_in) * 1000,
      };

      sessionStorage.setItem(
        STORAGE.tokens,
        JSON.stringify(stored),
      );

      return stored;
    }

    function describeTokens(tokens) {
      return [
        "Authenticated.",
        `Token type: ${tokens.token_type ?? "unknown"}`,
        `Expires in: ${tokens.expires_in ?? "unknown"} seconds`,
        `Expires at: ${
          tokens.expires_at
            ? new Date(tokens.expires_at).toISOString()
            : "unknown"
        }`,
        `Refresh token available: ${
          tokens.refresh_token ? "yes" : "no"
        }`,
      ].join("\n");
    }

    async function tokenRequest(config, parameters) {
      const response = await fetch(
        `${config.foundryUrl}/multipass/api/oauth2/token`,
        {
          method: "POST",
          headers: {
            "Content-Type":
              "application/x-www-form-urlencoded",
          },
          body: new URLSearchParams(parameters),
        },
      );

      const body = await response.text();

      if (!response.ok) {
        throw new Error(
          `Token request failed (${response.status}): ${body}`,
        );
      }

      try {
        return JSON.parse(body);
      } catch {
        throw new Error(
          `Token endpoint returned invalid JSON: ${body}`,
        );
      }
    }

    async function beginFoundryLogin() {
      const config = getConfig();

      const state = randomValue();
      const verifier = randomValue();
      const challenge = base64Url(
        await sha256(verifier),
      );

      saveConfig(config);

      sessionStorage.setItem(STORAGE.state, state);
      sessionStorage.setItem(STORAGE.verifier, verifier);

      const authorizeUrl = new URL(
        "/multipass/api/oauth2/authorize",
        config.foundryUrl,
      );

      const parameters = new URLSearchParams({
        response_type: "code",
        client_id: config.clientId,
        redirect_uri: config.redirectUri,
        scope: config.scopes,
        state,
        code_challenge: challenge,
        code_challenge_method: "S256",
      });

      // Realm belongs only on the authorization request.
      if (config.realm) {
        parameters.set("_realm", config.realm);
      }

      authorizeUrl.search = parameters.toString();

      window.location.assign(authorizeUrl.toString());
    }

    async function finishFoundryLogin() {
      const callback = new URL(window.location.href);

      const oauthError =
        callback.searchParams.get("error");

      if (oauthError) {
        const description =
          callback.searchParams.get("error_description") ||
          oauthError;

        throw new Error(
          `Authorization failed: ${description}`,
        );
      }

      const code = callback.searchParams.get("code");
      const returnedState =
        callback.searchParams.get("state");

      const expectedState =
        sessionStorage.getItem(STORAGE.state);

      const verifier =
        sessionStorage.getItem(STORAGE.verifier);

      if (
        !code ||
        !returnedState ||
        !expectedState ||
        !verifier ||
        returnedState !== expectedState
      ) {
        throw new Error(
          "Invalid OAuth callback or state mismatch.",
        );
      }

      const config = getConfig();

      // _realm is intentionally not included here.
      const tokens = await tokenRequest(config, {
        grant_type: "authorization_code",
        client_id: config.clientId,
        redirect_uri: config.redirectUri,
        code,
        code_verifier: verifier,
      });

      const stored = storeTokens(tokens);

      sessionStorage.removeItem(STORAGE.state);
      sessionStorage.removeItem(STORAGE.verifier);

      history.replaceState(
        {},
        document.title,
        callback.pathname,
      );

      return stored;
    }

    async function getFoundryAccessToken() {
      const serialized =
        sessionStorage.getItem(STORAGE.tokens);

      if (!serialized) {
        await beginFoundryLogin();

        return new Promise(() => {});
      }

      const tokens = JSON.parse(serialized);

      if (
        (tokens.expires_at ?? 0) >
        Date.now() + 30_000
      ) {
        setStatus(
          describeTokens(tokens),
          "success",
        );

        return tokens.access_token;
      }

      if (!tokens.refresh_token) {
        sessionStorage.removeItem(STORAGE.tokens);
        await beginFoundryLogin();

        return new Promise(() => {});
      }

      const config = getConfig();

      try {
        // _realm is also intentionally absent during refresh.
        const refreshed = await tokenRequest(config, {
          grant_type: "refresh_token",
          client_id: config.clientId,
          refresh_token: tokens.refresh_token,
        });

        const stored = storeTokens({
          ...refreshed,
          refresh_token:
            refreshed.refresh_token ??
            tokens.refresh_token,
        });

        setStatus(
          describeTokens(stored),
          "success",
        );

        return stored.access_token;
      } catch (error) {
        sessionStorage.removeItem(STORAGE.tokens);

        throw new Error(
          `${error.message}\nLog in again to obtain a new token.`,
        );
      }
    }

    function shellQuote(value) {
      return `'${String(value).replaceAll(
        "'",
        "'\"'\"'",
      )}'`;
    }

    async function generateCurl() {
      const config = getConfig();
      const apiValue = elements.apiUrl.value.trim();

      if (!apiValue) {
        throw new Error("Enter an API URL or path.");
      }

      saveConfig(config);

      const target = new URL(
        apiValue,
        config.foundryUrl,
      );

      if (
        target.origin !==
        new URL(config.foundryUrl).origin
      ) {
        throw new Error(
          "The API URL must use the configured Foundry host.",
        );
      }

      const accessToken =
        await getFoundryAccessToken();

      const command = [
        "curl --fail-with-body --silent --show-error \\",
        ` -H ${shellQuote(
          `Authorization: Bearer ${accessToken}`,
        )} \\`,
        ` ${shellQuote(target.toString())}`,
      ].join("\n");

      elements.curlOutput.textContent = command;

      setStatus(
        "curl command generated successfully.",
        "success",
      );
    }

    async function copyCurl() {
      const command =
        elements.curlOutput.textContent;

      if (
        !command ||
        command === "No command generated."
      ) {
        throw new Error(
          "Generate a curl command first.",
        );
      }

      await navigator.clipboard.writeText(command);

      setStatus(
        "curl command copied.",
        "success",
      );
    }

    function clearSession() {
      Object.values(STORAGE).forEach((key) => {
        sessionStorage.removeItem(key);
      });

      elements.curlOutput.textContent =
        "No command generated.";

      setStatus("Local OAuth session cleared.");
    }

    function run(action) {
      return async () => {
        try {
          await action();
        } catch (error) {
          console.error(error);

          setStatus(
            error.message ?? String(error),
            "error",
          );
        }
      };
    }

    document
      .querySelector("#loginButton")
      .addEventListener(
        "click",
        run(beginFoundryLogin),
      );

    document
      .querySelector("#tokenButton")
      .addEventListener(
        "click",
        run(async () => {
          await getFoundryAccessToken();
        }),
      );

    document
      .querySelector("#generateButton")
      .addEventListener(
        "click",
        run(generateCurl),
      );

    document
      .querySelector("#copyButton")
      .addEventListener(
        "click",
        run(copyCurl),
      );

    document
      .querySelector("#clearButton")
      .addEventListener(
        "click",
        clearSession,
      );

    async function initialize() {
      loadConfig();

      const callback = new URL(window.location.href);

      if (
        callback.searchParams.has("code") ||
        callback.searchParams.has("error")
      ) {
        setStatus("Completing OAuth callback…");

        try {
          const tokens =
            await finishFoundryLogin();

          setStatus(
            describeTokens(tokens),
            "success",
          );
        } catch (error) {
          console.error(error);

          setStatus(
            error.message ?? String(error),
            "error",
          );
        }

        return;
      }

      const serialized =
        sessionStorage.getItem(STORAGE.tokens);

      if (serialized) {
        try {
          setStatus(
            describeTokens(JSON.parse(serialized)),
            "success",
          );
        } catch {
          sessionStorage.removeItem(STORAGE.tokens);
        }
      }
    }

    initialize();
  </script>
</body>
</html>

```

Put the file in a folder, navigate to this folder with your terminal, and start serving it via `python3 -m http.server 8000`

Access `http://localhost:8000/oauth_test.html` and you should get:

 ![image](https://canada1.discourse-cdn.com/palantir/original/2X/7/768ac92279cd71669c2917d04eb96bf650a481a3.png)

Fill the configuration, namely from the Developer console application:

 ![image](https://canada1.discourse-cdn.com/palantir/original/2X/7/71643912d13b9cf3ee8b9584a0d7c1b01f91f5e4.png)

You can click “login” to start the OAuth flow, and see what a user would see (Do they see the OSDK approval screen ? Their IDP screen ? Something else ?).

Once logged-in, a token should be retrieved. You can then try to hit `/api/v2/ontologies` and get a curl generated, which you can then test from your terminal.

You should have a curl that look like:

```curl
curl --fail-with-body --silent --show-error \
  -H 'Authorization: Bearer ey...REDACTED...Q' \
  'https://REDACTED.com/api/v2/ontologies'

```

Result:

```json
{"data":[{"apiName":"ontology-d2770208-d11e-42d5-8708-67835767f0d6","displayName":"Advisory Ontology","description":" Ontology","rid":"ri.ontology.main.ontology.0bf6a0db-1234-5678-9413-58ee7d3edf3e"}]} 

```

See [https://www.palantir.com/docs/foundry/consumer-mode/foundry-consumer-setup/](https://www.palantir.com/docs/foundry/consumer-mode/foundry-consumer-setup/) for more details
