Closed aaronbartell closed 3 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 ?
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);
}
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 ?
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?
Could you explain why IoC should not be used here?
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.
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?
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.
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
Note however that an extension could use a path other than services... Maybe we would need a registry of such routes
You can look at the terminal-backend-contribution.ts for an example of a BackendApplicationContribution that uses
configure
andstart
Great explanation (one for the wiki) and thanks for giving me a place to start.
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?
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); }); }
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?
@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..
@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.
@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.
@akosyakov I also need to integrate StreetSide software's code spell-checker vscode extension somehow. Can I follow the same pattern for that?
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 ?
@rhildred there is documentation on developing Theia extensions: https://www.theia-ide.org/doc/Authoring_Extensions.html It should cover your questions.
@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.
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.
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/
I published theia-middleware
package https://www.npmjs.com/package/theia-middleware
Referenced to @ilstarno post
@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?
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!!!")
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.
It sounds strange, we did not change anything about backend application lifecycle. Could someone share a GitHub repo to reproduce from sources?
@akosyakov hi repository is https://bitbucket.org/vaxy/theta-middleware/src/master/
@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.
@akosyakov okay cool here imported https://github.com/ordinaryparksee/theia-middleware thank you
I've updated to latest next and cannot reproduce your issue:
a pr for your repo which i used: https://github.com/ordinaryparksee/theia-middleware/pull/1
~Oh.. i see~ ~then, maybe there is a conflict with some other extensions~ ~i'll posting soon when i found it~
I misunderstood you updated package.json
!
Thank you for check, and sorry
Example of authorization with cookies:
@osbre Does it work for web sockets as well?
@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
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?
There is no app, Theia is only a framework. Which product do you use? Example applications are not for production use.
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.