HTTP Request with OAuth1 API Authentication fails

Describe the problem/error/question

Hi. I am new here and start learning n8n. I want to create an HTTP Request node to connect to OSCAR EMR API (open source medical software) which uses REST with OAuth1.0a authentication. In N8N in Generic auth type, I selected OAuth1 API and completed all the fields in the credential window. When I click on “connect my account” button I get the error “OAuth Authorization Error. There was a problem generating the authorization URL. Request failed with status code 400”. Everything is correct because I tested the connection with Postman.
According to OAuth1.0a documentation during the authorization process I suppose to authorize n8n and get the oauth_verifier, and complete the process and get the authorization token. I think the authentication process breaks when I cannot authorize n8n and get the verifier.
Is there another way to connect to this OSCAR EMR API?
Thank you in advance.

What is the error message (if any)?

OAuth Authorization Error

There was a problem generating the authorization URL
Request failed with status code 400

Please share your workflow

(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)

Share the output returned by the last node

Information on your n8n setup

  • n8n version: 2.4.5
  • Database (default: SQLite):
  • n8n EXECUTIONS_PROCESS setting (default: own, main):
  • Running n8n via (Docker, npm, n8n cloud, desktop app): Docker
  • Operating system: Ubuntu 24.04

Hi @mavemania, welcome to the n8n community :tada: ! In practice I solve this by skipping Connect my account and using the HTTP Request node with Authentication set to None, manually signing the OAuth1 request just like Postman does. OSCAR doesnt support the full OAuth 1.0a authorization flow required by the OAuth1 Generic credential.

How do I manually sign the OAuth1 request in N8N? Can you please elaborate a bit? I am fairly new to N8N and still learning the basiscs. Thank you for your quick answer.

@mavemania

By “manual OAuth”, I’m referring to using the HTTP Request node with a custom Authorization header. In this setup, OAuth1 signing is handled outside the n8n credential system, which is a common approach when an API does not fully match the OAuth1 Generic flow. For reference, see the n8n documentation.:

Hope this helps. If you still need any help, feel free to reply.

I have been going through documentation but I wasn’t able to make it work. Can you please give an example how the header should look like? I have all the info used in OAuth1 generic credential. If it is easier I can use the permanent token obtained with Postman, but so far I can’t figure it out. Everything I do it ends up with “unauthorized” error.

Hi @mavemania,

Happy to walk you through the manual OAuth setup. Here’s the basic flow:

  1. Add a Code node to generate the OAuth1 signature using crypto libraries
  2. Build the Authorization header string with all OAuth parameters
  3. Pass that header to your HTTP Request node using Header Auth

The tricky part is usually getting the signature base string and HMAC-SHA1 encoding right. Would it help if I shared a sample Code node that handles the OAuth1 signing?

Hi @mavemania, welcome!

One quick thing to check first, Is your “OSCAR EMR API” hosted locally or exposed with public Auth/Access links?

if you are running n8n via Docker, it cannot access your host machine using localhost, If that is the case, the authentication is likely failing simply because n8n cannot reach the endpoint..

Hi TAmmy. I actually tried that and when use the crypto library it tells me the crypto is disallowed. I am using version 2.4.5

Hi Mohamed. The OSCAR EMR is exposed with public links.

1 Like

Very nice!
It should be straightforward, You shouldn’t need any code nodes or manual requests at all (this is most likely AI-generated advice), n8n handles authentication for you natively, You just need to fill in the credentials/URLs correctly,

I haven’t worked with this API before, but after doing some research, I found this documentation,

so if this is the API you’re using, I would configure the authentication like this:

It is also important to check the Callback URL and make sure it is registered correctly in your OSCAR application settings..

This doesn’t work. Every field is completed correctly but it doesn’t work, because during the authorizing process a webpage should come up to authorize n8n. This one doesn’t come up and that is why I get the “unauthorized error. OSCAR uses Oauth1a, which is a bit different then OAuth1. Like Tamy said above OSCAR doesn’t support the full OAuth 1.0a authorization flow required by the OAuth1 Generic credential.

1 Like

TL;DR: The OAuth1 settings are correct, but OSCAR’s OAuth 1.0a flow is stateful and cookie-dependent (it requires cookies from /ws/oauth/initiate to be reused during /ws/oauth/authorize). n8n’s OAuth1 Generic is effectively stateless, so the authorization never completes and requests stay 401, even with the right URLs and HMAC-SHA1. Manual signing in a Code node won’t work either because crypto is blocked in recent n8n versions. The reliable fixes are to run an OAuth helper on the OSCAR domain (to preserve cookies) or use an external OAuth1 signer/proxy after completing OAuth once outside n8n.

Full Answer:

The OAuth configuration is correct, but OSCAR’s OAuth 1.0a implementation is stateful and incompatible with n8n’s OAuth1 Generic credential.

OSCAR explicitly requires that cookies returned from /ws/oauth/initiate be reused during /ws/oauth/authorize. n8n’s OAuth1 Generic flow is effectively stateless: it does not persist session cookies (or request-token secrets reliably) between the initiate call and the browser-based authorization step. Because of this, the authorization never completes and all API calls remain 401 Unauthorized, even with correct URLs, keys, and HMAC-SHA1.

This is not a misconfiguration and not user error.

Why common workarounds fail

  • OAuth1 Generic → cannot satisfy OSCAR’s cookie/session requirement

  • Manual signing in a Code node → crypto is intentionally blocked in recent n8n versions

  • Re-entering URLs/keys → does not address the flow mismatch

What actually works

  1. OAuth helper hosted on the OSCAR domain (cleanest OAuth flow)
    Run a small helper under the same hostname as OSCAR so browser cookies are preserved:

    • Browser → helper → /ws/oauth/initiate (cookies set)

    • Browser → /ws/oauth/authorize (cookies reused)

    • Browser → helper callback → /ws/oauth/token
      The helper stores or outputs the access token + secret. n8n does not perform OAuth afterward.

  2. External OAuth1 signer or proxy (most practical with n8n)
    Complete OAuth once outside n8n (e.g., helper or Postman), then use a small service to sign or proxy OSCAR requests. n8n calls this service via HTTP.

  3. Custom n8n node / patch
    Extending n8n to persist cookies and request-token secrets would allow native support, but this requires code changes beyond the Generic credential.

Bottom line: the configuration is right; the failure is a protocol/flow incompatibility. Use a same-domain OAuth helper or an external signer/proxy.


References

  • OSCAR / WorldEMR API documentation — states the client must manage cookies between initiate and authorize steps:
    https://worldemr.org/knowledge-base/oscar-emr-api/

  • n8n OAuth1 limitations — documented issues with strict/stateful OAuth1 providers and token persistence:
    https://github.com/n8n-io/n8n/issues

  • n8n Code node sandboxing — crypto blocked by design in recent versions, preventing in-node OAuth1 signing:
    n8n docs & release notes (Code node / runners)

Everything makes sense.I would go for most practical solution, but I don’t know how to create this service to make n8n to call it to sign Oscar requests. I did complete Oauth once using Postman and got the permanent token+secret, but not sure how to use that token with n8n.

Run a small internal HTTP service (Node is easiest) that:

  • receives { method, path/full URL, query/body }

  • signs the request using OAuth1 HMAC-SHA1

  • forwards the request to OSCAR

  • returns OSCAR’s response

n8n calls this service via a normal HTTP Request node instead of calling OSCAR directly

So the flow becomes: n8n → signer/proxy → OSCAR → signer/proxy → n8n

n8n never needs to store OAuth1 secrets in a credential, never opens an authorization page, and never needs crypto.

This is a very common pattern for legacy OAuth1 APIs and should work reliably with OSCAR if you already have the permanent token + secret.

Here’s a minimal, Docker-friendly Node OAuth1 signer/proxy you can run next to n8n. It takes a simple JSON payload from n8n, signs the request with your consumer key/secret + Postman access token/secret, forwards it to OSCAR, and returns the raw response.

1) Create these 3 files in a folder (e.g. oscar-oauth-proxy/)

docker-compose.yml

services:
  oscar-oauth-proxy:
    build: .
    container_name: oscar-oauth-proxy
    environment:
      PORT: "3000"

      # OSCAR base URL (no trailing slash is best)
      OSCAR_BASE_URL: "https://example.com/oscar"

      # From OSCAR REST Clients registration
      CONSUMER_KEY: "YOUR_CONSUMER_KEY"
      CONSUMER_SECRET: "YOUR_CONSUMER_SECRET"

      # From Postman (permanent token + secret)
      ACCESS_TOKEN: "YOUR_ACCESS_TOKEN"
      ACCESS_TOKEN_SECRET: "YOUR_ACCESS_TOKEN_SECRET"

      # Optional - Recommended: shared secret auth for the proxy endpoint
      PROXY_SHARED_SECRET: "change-me"
    ports:
      - "3000:3000"
    restart: unless-stopped

Dockerfile

FROM node:20-alpine

WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci || npm i

COPY server.js ./

EXPOSE 3000
CMD ["node", "server.js"]

package.json

{
  "name": "oscar-oauth1-proxy",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "express": "^4.19.2",
    "node-fetch": "^3.3.2",
    "oauth-1.0a": "^2.2.6"
  }
}

server.js

import express from "express";
import fetch from "node-fetch";
import OAuth from "oauth-1.0a";
import crypto from "crypto";

const {
  PORT = 3000,
  OSCAR_BASE_URL,
  CONSUMER_KEY,
  CONSUMER_SECRET,
  ACCESS_TOKEN,
  ACCESS_TOKEN_SECRET,
  PROXY_SHARED_SECRET,
} = process.env;

for (const k of ["OSCAR_BASE_URL","CONSUMER_KEY","CONSUMER_SECRET","ACCESS_TOKEN","ACCESS_TOKEN_SECRET"]) {
  if (!process.env[k]) throw new Error(`Missing env var: ${k}`);
}

const oauth = new OAuth({
  consumer: { key: CONSUMER_KEY, secret: CONSUMER_SECRET },
  signature_method: "HMAC-SHA1",
  hash_function(baseString, key) {
    return crypto.createHmac("sha1", key).update(baseString).digest("base64");
  },
});

const token = { key: ACCESS_TOKEN, secret: ACCESS_TOKEN_SECRET };

const app = express();
app.use(express.json({ limit: "2mb" }));

/**
 * POST /oscar
 * Headers:
 *   x-proxy-secret: <PROXY_SHARED_SECRET>   (if configured)
 * Body:
 *   {
 *     "method": "GET"|"POST"|"PUT"|"DELETE",
 *     "path": "/ws/rs/...." OR full URL,
 *     "query": { ... },          // optional
 *     "body":  { ... } | "raw",  // optional
 *     "headers": { ... }         // optional
 *   }
 */
app.post("/oscar", async (req, res) => {
  try {
    // Optional shared-secret protection
    if (PROXY_SHARED_SECRET) {
      const got = req.header("x-proxy-secret");
      if (!got || got !== PROXY_SHARED_SECRET) {
        return res.status(401).json({ error: "Unauthorized (proxy secret)" });
      }
    }

    const method = String(req.body?.method || "GET").toUpperCase();
    const pathOrUrl = req.body?.path;
    const query = req.body?.query || {};
    const body = req.body?.body;
    const extraHeaders = req.body?.headers || {};

    if (!pathOrUrl || typeof pathOrUrl !== "string") {
      return res.status(400).json({ error: "Missing 'path' (string)" });
    }

    // Build URL: accept either full URL or a path appended to OSCAR_BASE_URL
    let url;
    if (/^https?:\/\//i.test(pathOrUrl)) {
      url = new URL(pathOrUrl);
    } else {
      const base = OSCAR_BASE_URL.replace(/\/+$/, "");
      url = new URL(base + pathOrUrl);
    }

    for (const [k, v] of Object.entries(query)) {
      url.searchParams.set(k, String(v));
    }

    const requestData = { url: url.toString(), method };
    const authHeader = oauth.toHeader(oauth.authorize(requestData, token));

    const headers = {
      Accept: "application/json, */*",
      ...extraHeaders,
      ...authHeader,
    };

    const opts = { method, headers };

    // Add body if not GET/HEAD
    if (body !== undefined && method !== "GET" && method !== "HEAD") {
      // default to JSON if body is an object
      if (typeof body === "object" && body !== null && !Buffer.isBuffer(body)) {
        headers["Content-Type"] = headers["Content-Type"] || "application/json";
        opts.body = JSON.stringify(body);
      } else {
        headers["Content-Type"] = headers["Content-Type"] || "text/plain";
        opts.body = String(body);
      }
    }

    const r = await fetch(url.toString(), opts);

    // Pass through response status + content-type
    const contentType = r.headers.get("content-type") || "text/plain";
    const text = await r.text();

    res.status(r.status);
    res.setHeader("content-type", contentType);
    res.send(text);
  } catch (e) {
    res.status(500).json({ error: String(e?.message || e) });
  }
});

app.listen(PORT, () => {
  console.log(`OSCAR OAuth1 proxy listening on :${PORT}`);
});

2) Run it

From that folder:

docker compose up -d --build

Quick smoke test

If it starts and listens, run:

curl -X POST http://localhost:3000/oscar \
  -H "Content-Type: application/json" \
  -H "x-proxy-secret: change-me" \
  -d '{"method":"GET","path":"/ws/rs/patient/12345"}'

If that returns a 200/401 from OSCAR, your proxy wiring is correct and you can move to n8n.

3) Call it from n8n (single HTTP Request node)

HTTP Request Node

  • Method: POST

  • URL: http://oscar-oauth-proxy:3000/oscar
    (or http://<host-ip>:3000/oscar depending on networking)

  • Headers:

    • x-proxy-secret: change-me (if you set PROXY_SHARED_SECRET)

    • Content-Type: application/json

  • Body (JSON):

{
  "method": "GET",
  "path": "/ws/rs/patient/12345"
}

With query params:

{
  "method": "GET",
  "path": "/ws/rs/something",
  "query": { "startDate": "2026-01-01", "endDate": "2026-01-31" }
}

If the proxy is reachable, you’ll get one of these outcomes:

  • 200 + data → it works

  • 401 from proxy → missing/wrong x-proxy-secret

  • 500 from proxy → proxy env vars wrong / token invalid / upstream error

  • Connection refused / timeout → n8n cannot reach the proxy URL (networking)

Notes

  • If n8n is also in Docker, put both services on the same Docker network (Compose does this automatically if they’re in the same compose file, or you can attach networks).

  • Keep this proxy private (LAN/VPN) or protected with the x-proxy-secret header.

If anyone knows a better way or can see flaws in my answer please speak up as I’d genuinely like to learn.

1 Like

Thank you so much Michael. It works!!! And I learned a lot of stuff along the way.

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.