wandb / openui

OpenUI let's you describe UI using your imagination, then see it rendered live.
https://openui.fly.dev
Apache License 2.0
18.84k stars 1.73k forks source link

Basic auth configured in nginx reverse proxy not working due to "Authorization: Bearer sk-fake" generated by OpenUI frontend #140

Closed hukarere closed 3 months ago

hukarere commented 4 months ago

Hello,

I'm trying to set up OpenUI on a VPS for remote access. I'm running it as a local python script without docker, and in order to access it remotely, I have set up a nginx reverse proxy to pass the incoming HTTP requests to http://127.0.0.1:7878 using the config proposed in this issue: https://github.com/wandb/openui/issues/40 . This worked perfectly.

However, I don't have a set of fixed IPs to restrict access, so I tried to use HTTP basic auth in nginx to restrict access with user/password. This also works for all HTTP requests for acessing OpenUI, with the exception of requests initiated when you type a prompt to LLM: typing such prompts initiates an HTTP request to http://127.0.0.1:7878/v1/chat/completions with the header "Authorization: Bearer sk-fake". Apparently, OpenUI implements an OpenAI-compatible API and uses such requests when communicating between the frontend and the backend. And here is the source of my problem, which I have been trying to solve without any success: when the frontend adds the header "Authorization: Bearer sk-fake", it overrides the header "Authorization: Basic BASIC_AUTH_STRING" generated by the browser in order to pass basic auth required by nginx, so nginx returns "401 Not authorized".

It seems that modifying the frontend to not generate the "Authorization: Bearer sk-fake" header can't help because here is how it is generated in frontend/src/api/openai.ts:

const openai = new OpenAI({
apiKey: 'sk-fake',
  baseURL: `${host()}/v1`,
    dangerouslyAllowBrowser: true
})

I guess that in the absence of the API key (even if it's fake), new OpenAI() just won't work as expected because the backend refuses OpenAI-compatible API requests without this fake API key.

So, my question is, is there any easy way to modify the frontend and the backend to pass the fake API key using some method other than the "Authorization:" header that would not interfere with basic auth attempted by the browser when sending HTTP requests to the nginx reverse proxy?

Actually, it seems that given that the API key is hard-coded as "sk-fake", perhaps it would be easier to modify the frontend and backend to not pass this key at all, but behave as if this fake key is being passed?

P.S.

Googling for this issue, I have encountered a lot of questions from other people suffering from the impossibility to combine HTTP basic auth with token authorization via "Authorization: Bearer XXX" used in lots of apps/frameworks (not OpenUI), but I have not found any satisfactory answer on how to solve this problem.

vanpelt commented 4 months ago

Hey @hukarere thanks for the detailed description of your issue, this all makes sense. My backend doesn't actually check this api key or need it, it's only set because the OpenAI typescript SDK requires that it's set. I won't be able to try this, but the trick will be modifying the OpenAI SDK to pass the header we want. I imagine you can do this by changing the JS you mentioned in your description to something like:

class MyOpenAI extends OpenAI {
    protected override authHeaders(opts) {
       return {};
    }
}
const openai = new MyOpenAI({
  apiKey: 'sk-fake',
  baseURL: `${host()}/v1`,
  dangerouslyAllowBrowser: true
})

Not sure if that will work, I found the raw source code for the client here. I figured by just not including the auth header the browser will pass the existing basic auth header but you may need to muck with the fetch options for that to work. Otherwise you could return an Authorization: Basic xxxx in that method which could work.

I also found this in the source code which could be another thing to try if the method I gave you doesn't work.

hukarere commented 4 months ago

@vanpelt, thank you for your suggestion! I know python, but not javascript (only the most trivial things), so I'm not sure if I will be able to debug it further if your proposed change does not work, but I'll try it and ask for your help if it doesn't work, thank you!

vanpelt commented 4 months ago

ChatGPT could be helpful here 😉 . Let me know how it goes 🤞

vanpelt commented 4 months ago

I did a little more digging, my approach above indeed removes the Authorization header but it probably won't automatically include the existing existing credentials in the fetch request. To make that work this *might work, haven't tested but this is a good starting point. The key is telling the fetch api to include any existing credentials in the request.

class MyOpenAI extends OpenAI {
    protected override authHeaders(opts) {
       return {};
    }
}
function myfetch(url, opts) {
  return fetch(url, {...opts, credentials: "include"})
}
const openai = new MyOpenAI({
  apiKey: 'sk-fake',
  baseURL: `${host()}/v1`,
  dangerouslyAllowBrowser: true,
  fetch: myfetch
})
hukarere commented 4 months ago

Hi @vanpelt,

(1)

Your proposed solution worked like a charm without any additional modifications, thank you!

Now the basic auth is working. Will you please add your change to the repo?

(2)

I have one question, though, about fetch(url, {...opts, credentials: "include"}): what are those credentials? As far as I understand, ~OpenAI~ OpenUI lacks any user authentication or authorization... If I comment out fetch: myfetch and use the standard fetch(), everything seems to keep working correctly.

I know that the live demo does have user authentication via github, but I'm not sure how it is done and this is not done in the locally running version. Maybe those credentials are used in the live demo?

If I knew how to create users directly in OpenUI and force them to login to access the locally running version (like in the live demo), then there would be no need for basic auth. Please let me know.

vanpelt commented 4 months ago

credentials: "include" tells the browser to include any credentials that it has with the requests. I believe because we're on the same domain it just includes those automatically, we would only need this if the server was running on a different domain.

When operating the application in hosted mode you can do user authentication via github. That's the only auth method I support when it's enabled. You would need to create a github OAuth app and specify GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET. I may add instructions for this eventually. Anyway, I just pushed the fix to the main branch so you can continue using basic auth for now.

hukarere commented 4 months ago

credentials: "include" tells the browser to include any credentials that it has with the requests. I believe because we're on the same domain it just includes those automatically, we would only need this if the server was running on a different domain.

But what are those credentials, anyway? Github credentials? Or maybe "credentials" is some node.js concept of which I am not aware?

hukarere commented 4 months ago

Anyway, I just pushed the fix to the main branch so you can continue using basic auth for now.

Thank you! I have just tried to use the new version. The fix is applied, but it only works if the frontend is rebuilt with 'npm run build'. Will you please add the rebuilt frontend to the repo for it to work right away after 'git clone'?

hukarere commented 4 months ago

And by the way, your fix does not include myfetch(), still not sure if it's useful or not...

vanpelt commented 4 months ago

I'll rebuild later this week

vanpelt commented 3 months ago

This is now rebuilt!

hukarere commented 3 months ago

@vanpelt,

This is now rebuilt!

Thank you, confirmed!