eclipse-theia / theia

Eclipse Theia is a cloud & desktop IDE framework implemented in TypeScript.
http://theia-ide.org
Eclipse Public License 2.0
19.86k stars 2.48k forks source link

Authentication #411

Closed aaronbartell closed 3 years ago

aaronbartell commented 7 years ago

It would be great if there were mechanisms to automate authentication of the browser-based Theia so it can be accessed in a secured fashion via API.

My scenario for using Theia is in a bigger web application that has a need for a file explorer, code editor, and terminal. In my web application a user will click a button to open Theia to their specific container. At that point I will check to see if Theia is running and if not I will start it and include a passphrase that my web application will use to make it so only the current user can gain access to this particular web URL.

Side Note: HTTP Basic Auth is no longer a good approach because Chrome blocks it when you try to put them credentials on the URL (i.e. http://user:password@site.com:1234/path). Chrome, as of today, will issue an error when the page attempts to load resources (css, js, etc).

It seems JSON Webtokens are all the rage. I've not used them yet. The best approach is probably to allow for authentication extensions/plugins. Then Theia could ship with some popular default auth implementations but allow others to develop their own.

hexa00 commented 7 years ago

We wanted to leave this out of Theia and for example use this technique: https://github.com/auth0/nginx-jwt

Leaving the auth part to the webserver / reverse proxy.

Does that look like a possible path to you ?

aaronbartell commented 7 years ago

In my scenario I will have a single machine with many of instances of Theia Node.js server running. Each Theia server could belong to a different user and would need different credentials.

I briefly read through nginx-jwt and I believe I would need a separate Nginx server for each instance of Theia for my scenario. I'd prefer to not add Nginx to the stack of each container.

If you would like to keep authentication out of Theia I am wondering if we could at least document how somebody could add it manually (I am still new to the Theia code base and learning how it is structured).

For example, could authentication be added as Express middleware in the following location inside examples/browser/src-gen/backend/server.js?

function start(port, host) {
    const application = container.get(BackendApplication);
    application.use(express.static(path.join(__dirname, '../../lib'), {
        index: 'index.html'
    }));
    application.use(function(){   <----------------------
     // my auth strategy
    });
    return application.start(port, host);
}
hexa00 commented 7 years ago

We have not considered authentification much yet. So anything is possible but we would like to keep it out of the Theia code as much as possible.

Your idea sounds possible it would need to be investigated more. We're open to proposals...

Note also that I used nginx-jwt only as an example, I thought for a production deployment one would use another server than express anyway in a reverse proxy configuration so any webserver would do.

If I may why would you need more than one instance of nginx ?

aaronbartell commented 7 years ago

If I may why would you need more than one instance of nginx ?

I will have hundreds of users on a single instance of the operating system, each operating in their own container(n1). Each container has a browser-based IDE/terminal that the user can navigate to from their dashboard. When they click on the dashboard link I start up the IDE/terminal and only allow the user in if they have the correct HTTP Basic Authentication credentials. Each container needs to have separate credentials so other devs on the same machine can't access another's IDE/terminal.

n1 - using chroot, don't laugh :-)

I could have a reverse proxy server do the authentication, but that's the only function it would serve so I am wanting to have an extension in Theia to accomplish the task.

Your idea sounds possible it would need to be investigated more. We're open to proposals...

I like your focus on extensibility and am thinking extensions could also be allowed during server startup, though IoC via inversify might not be the approach you want because you don't want to have an authentication IoC interface. But what if there was a directory where Express middleware was stored and subsequently loaded at startup, and if someone desired they could include their own authentication middleware.

This SO post shows how Express routes stored in multiple files can be recursively loaded. The same could be done for Express middleware.

I propose to have a /middleware/startup directory where custom .js files could be placed.

Thoughts?

svenefftinge commented 7 years ago

Could you explain why IoC should not be used here?

aaronbartell commented 7 years ago

Could you explain why IoC should not be used here?

I probably shouldn't have opened that can of worms given my limited understanding of Theia's implementation of IoC.

My perspective (which may be incorrect): To have IoC you usually need an interface that declares what an eventual implementation of an interface (OO interface) needs to look like. For example, Theia currently has the Clipboard Service. My assumption is that if Theia used IoC (inversify) to implement allowance of authentication then Theia would inherently have authentication as a feature, which is what @hexa00 mentioned you don't want.

So I was trying to come up with ideas for implementing authentication that didn't require Theia to declare support for authentication. Maybe that can be done with IoC, I don't know. My mind sent me down the route of Express middleware, which could be generically/dynamically loaded by looking at a specific directory.

More... I am now seeing that the usage of Websockets in Theia will require authentication to go beyond the implementation of Express middleware because the Websocket communication would also need to be authenticated (I believe). So auth for Theia just become more complicated than I had originally envisioned. Sorry for not thinking through it more fully before posting.

svenefftinge commented 7 years ago

That's fine, we can think and explore together. I just wanted to understand your concerns regarding IoC. In fact we have something called a BackendApplicationContribution that gives you a callback to configure express. Wouldn't that be sufficient?

aaronbartell commented 7 years ago

In fact we have something called a BackendApplicationContribution that gives you a callback to configure express. Wouldn't that be sufficient?

Could you confirm whether my understanding of BackendApplicationContributions is correct... when Theia starts it will iterate through the ./packages directory to load extensions**; for example, it will load ./packages/terminal. If that's correct then the challenge I have is inserting Websocket authentication into the existing terminal communication; same with the editor, though I can't seem to locate its Websocket code unless it is using connection.ts.

**I am not sure if "extension" and "Contribution" and "packages" are synonyms.

Side Note: I am fairly new to TypeScript so my perusing of the code is doubly complicated. Thanks for your patience.

hexa00 commented 7 years ago

A contribution is one implementation of an interface that is to be called in a certain context. Multiple contributions for a particular context can be called such that all registred contributions are called for a specific contribution context.

For example: BackendApplicationContribution is defined in backend-applications.ts.

On creation of the BackendApplication (on app startup) all the BackendApplicationContributions will be called with the configure?(app: express.Application): void; method. Thus giving any BackendApplicationContribution a chance to configure the Express server.

Also once BackendApplication.start() is called (at the start of the main application) it will call the onStart?(server: http.Server): void; method of each BackendApplicationContribution. So that BackendApplications can start a websocket server.

So: extension: Can mean a Theia package as in what is in packages or possibly an implementation of a IoC interface. contribution: is what I just explained packages: refer to what is in packages/* since these are to be published on their own as an npm package.

Note that I'm not sure how the auth would work exactly with websocket etc but I think you could configure express such that the route services/* under wich all services are accessed could be setup such that it needs auth.

And such a config could be done by adding a BackendApplicationContribution and implementing the configure method.

You can look at the terminal-backend-contribution.ts for an example of a BackendApplicationContribution that uses configure and start

hexa00 commented 7 years ago

Note however that an extension could use a path other than services... Maybe we would need a registry of such routes

aaronbartell commented 7 years ago

You can look at the terminal-backend-contribution.ts for an example of a BackendApplicationContribution that uses configure and start

Great explanation (one for the wiki) and thanks for giving me a place to start.

jopit commented 6 years ago

Given the changes for https://github.com/theia-ide/theia/pull/1771, is creating a BackendApplicationContribution still the best way to implement authentication for Theia's websockets?

ilstarno commented 5 years ago

**this is the simplest solution which asks for user authentication ..

add this on server.js**

function start(port, host, argv) { if (argv === undefined) { argv = process.argv; }

const cliManager = container.get(CliManager);
return cliManager.initializeCli(argv).then(function () {
    const application = container.get(BackendApplication);
    application.use((req, res, next) => {

// ----------------------------------------------------------------------- // authentication middleware

const auth = {login: 'username_here', password: 'password_here'} // change this

// parse login and password from headers const b64auth = (req.headers.authorization || '').split(' ')[1] || '' const [login, password] = new Buffer(b64auth, 'base64').toString().split(':')

// Verify login and password are set and correct if (!login || !password || login !== auth.login || password !== auth.password) { res.set('WWW-Authenticate', 'Basic realm="401"') // change this res.status(401).send('Authentication required.') // custom message return }

// ----------------------------------------------------------------------- // Access granted... next()

}); application.use(express.static(path.join(__dirname, '../../lib'), { index: 'index.html' })); return application.start(port, host); }); }

rhildred commented 5 years ago

I did this @ilstarno. Ended up shooting myself in the foot because I used a Docker volume to substitute in my own server.js. When the generated server.js changed I had a debugging problem that I failed for a long time at because I didn't remember that I had messed with it.

@hexa00 I don't see how to register a BackendApplicationContribution. Is it as simple as adding my own module to the package.json?

ilstarno commented 5 years ago

@rhildred, first try to build the image normally, later on try to access at your docker container and inside the server.js file replace the start function with this `function start(port, host, argv) { if (argv === undefined) { argv = process.argv; }

const cliManager = container.get(CliManager);
return cliManager.initializeCli(argv).then(function () {
    const application = container.get(BackendApplication);
    application.use((req, res, next) => {

// ----------------------------------------------------------------------- // authentication middleware

const auth = {login: 'username_here', password: 'password_here'} // change this

// parse login and password from headers const b64auth = (req.headers.authorization || '').split(' ')[1] || '' const [login, password] = new Buffer(b64auth, 'base64').toString().split(':')

// Verify login and password are set and correct if (!login || !password || login !== auth.login || password !== auth.password) { res.set('WWW-Authenticate', 'Basic realm="401"') // change this res.status(401).send('Authentication required.') // custom message return }

// ----------------------------------------------------------------------- // Access granted... next()

}); application.use(express.static(path.join(__dirname, '../../lib'), { index: 'index.html' })); return application.start(port, host); }); }` save the file and restart your container and you should be good to go..

akosyakov commented 5 years ago

@ilstarno There is BackendApplicationContribution.configure callback to install additional express handers instead of modifying generated code. One can create a basic authentication extension using it.

rhildred commented 5 years ago

@akosyakov sorry to be dense about this. Perhaps it is all inversify magic. Do I just add a npm module of my own in the package.json that implements a BackendApplicationContribution interface with the .configure callback defined? Is there an example that I can follow.

rhildred commented 5 years ago

@akosyakov I also need to integrate StreetSide software's code spell-checker vscode extension somehow. Can I follow the same pattern for that?

rhildred commented 5 years ago

I think that it finally dawned on me. I do need to write a node package since, "A Theia extension is a node package declaring theiaExtensions property in package.json:"

{
  "theiaExtensions": [{
      "backend": "lib/myExtension/node/myextension-backend-module",
    }]
}

Then in myextension-backend-module I need to have the following:


import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';

@injectable()
export class AuthClass implements BackendApplicationContribution {

    configure(app: Application): void {
        app.use(....);
    }

}

Am I correct @akosyakov ?

akosyakov commented 5 years ago

@rhildred there is documentation on developing Theia extensions: https://www.theia-ide.org/doc/Authoring_Extensions.html It should cover your questions.

rhildred commented 5 years ago

@akosyakov You are right. It did answer my questions. I did yo theia-extension in an empty folder named authorization. Then in authorization/package.json I added a backend:

  "theiaExtensions": [
    {
      "frontend": "lib/browser/authorization-frontend-module",
      "backend": "lib/node/authorization-backend-module"
    }
  ]

in authorization/src/node/authorization-backend-module.ts

/**
 * Generated using theia-extension-generator
 */

import { AuthorizationContribution } from './authorization-contribution';
import {
    BackendApplicationContribution
} from "@theia/core/lib/node/backend-application";

import { ContainerModule } from "inversify";

export default new ContainerModule(bind => {
    // add your contribution bindings here

    bind(BackendApplicationContribution).to(AuthorizationContribution);

});

in authorization/src/node/authorization-contribution.ts

import { injectable} from "inversify";
import { Application } from 'express';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';

@injectable()
export class AuthorizationContribution implements BackendApplicationContribution {

    configure(app: Application): void {
        app.use("/test/ping/", (req, res) =>{
            res.end("ok");
        });
    }

}

It was relatively simple. Thanks for your help everyone.

cell commented 5 years ago

Hi. As the Issue is not closed I add my two cents. I think authentication should stay out of theia. As it's single user, I can't see why authentication would be needed.

For those wanting to have multiple instances of theia on the same host and have authentication, you'll need a reverse-proxy anyway otherwise each instance has to be mapped to a different port on the hosts.

Now the question is which reverse-proxy to use. Nginx is the default choice nowadays, I prefer traefik for containers: it's the container running with labels which configure traefik on the fly. Here is an example for a docker registry which needs authentication. It's in the same compose file but the registry could be started separately, like yet-an-other-theia for an other user.

akosyakov commented 4 years ago

Someone on Reddit gave a good answer how to secure access to Theia (any web app actually): https://www.reddit.com/r/selfhosted/comments/dclcv7/how_would_you_secure_access_to_a_web_ide_in_a_vps/f295nxw/

ordinaryparksee commented 4 years ago

I published theia-middleware package https://www.npmjs.com/package/theia-middleware Referenced to @ilstarno post

ordinaryparksee commented 4 years ago

@akosyakov i made theia-middleware for authentication but its working only latest version doesn't work with next. how can i adapt to next version? do you know any documentation about next version?

ordinaryparksee commented 4 years ago

I think this app.use is not called

@injectable()
export class TheiaMiddlewareContribution implements BackendApplicationContribution {

configure(app: Application): void {
    console.log("!!!DEBUG!!!")
    app.use((request, response, next) => {
        console.log("!!!DEBUG2!!!")
rhildred commented 4 years ago

I think that you are right. I had my authorization pattern working since my previous post (March 21, 2019). The next version no longer works in my environment either.

akosyakov commented 4 years ago

It sounds strange, we did not change anything about backend application lifecycle. Could someone share a GitHub repo to reproduce from sources?

ordinaryparksee commented 4 years ago

@akosyakov hi repository is https://bitbucket.org/vaxy/theta-middleware/src/master/

akosyakov commented 4 years ago

@ordinaryparksee Is there a way to export it to GitHub or GitLab. We are using Gitpod for development and it does not support bit butcket yet.

ordinaryparksee commented 4 years ago

@akosyakov okay cool here imported https://github.com/ordinaryparksee/theia-middleware thank you

akosyakov commented 4 years ago

I've updated to latest next and cannot reproduce your issue:

Screen Shot 2020-01-14 at 13 43 14

a pr for your repo which i used: https://github.com/ordinaryparksee/theia-middleware/pull/1

ordinaryparksee commented 4 years ago

~Oh.. i see~ ~then, maybe there is a conflict with some other extensions~ ~i'll posting soon when i found it~

ordinaryparksee commented 4 years ago

I misunderstood you updated package.json! Thank you for check, and sorry

osbre commented 4 years ago

Example of authorization with cookies:

https://github.com/brilliant-code/theia-cookie-auth

akosyakov commented 4 years ago

@osbre Does it work for web sockets as well?

osbre commented 4 years ago

@akosyakov,

Just tried to connect websockets with Theia and this plugin.

In Browser console:

conn = new WebSocket('ws://127.0.0.1:5000/services')
conn.send('{"kind":"open","id":0,"path":"/services/commands"}')

Response from Browser "Network" tab:

{"kind":"ready","id":0}

So, it looks likes auth doesn't work for websockets

posix4e commented 3 years ago

I disagree with this approach. We can't protect this at the external layer, the app must protect itself or it can't be launched on a shared cluster. Plus special attention of locking it's network connection out becomes important. Should we create a new issue for people who want the password built into the app itself?

akosyakov commented 3 years ago

There is no app, Theia is only a framework. Which product do you use? Example applications are not for production use.