wekan / ldap

LDAP support for Wekan code has been moved to https://github.com/wekan/wekan/tree/master/packages/wekan-ldap , issues to https://github.com/wekan/wekan/issues , and if PRs are needed please add them instead to https://github.com/wekan/wekan/pulls
https://github.com/wekan/wekan/tree/master/packages/wekan-ldap
MIT License
12 stars 10 forks source link

Client-Side (Mutual) Authentication #59

Open perlhub opened 5 years ago

perlhub commented 5 years ago

Is there support for client-side authentication? For example, to authenticate the user you request a client's certificate. Accounts would be created if the certs are trusted (based on a configured trust store).

For additional security, Wekan can connect to LDAP, find the user based on cert info and reject access unless they are in a configured group. You never need to request a password from the user.

I have LDAP setup, but I would love to fetch their client certificate instead of asking them to provide their account credentials. This is how I've setup access in the past with Apache for other webapps.

xet7 commented 5 years ago

You could look are there related settings at: https://github.com/wekan/wekan/blob/devel/docker-compose.yml

Wekan LDAP code is at: https://github.com/wekan/wekan-ldap

Wekan can be run behind Apache: https://github.com/wekan/wekan/wiki/Apache

@Akuket

Do you know about this?

perlhub commented 5 years ago

Thanks for the reply. I'm familiar with the links. I have a successful docker deployment with LDAP configured and running. Works great. But I want to take it a step further. Let's say I follow the third link above and setup a reverse proxy. I would add directives to fetch client certificates (if they have any):

# activate the client certificate authentication SSLCACertificateFile /etc/apache2/ssl/client-accepted-ca-chain.crt SSLVerifyClient optional SSLVerifyDepth 2

I would then create a session variable that forwards the certificate data to Wekan: RequestHeader set SSL_CLIENT_S_DN "%{SSL_CLIENT_S_DN}s"

My question now is how does wekan use SSL_CLIENT_S_DN to login (or create) the user? In other words, can Wekan automatically log-in users that have already been authenticated by the reverse proxy?

As a comparison, when I setup a MediaWiki server, I loaded the Extension:Auth remoteuser extension to add this functionality. I'm curious if this capability is available in Wekan?

xet7 commented 5 years ago

I presume it's not yet in Wekan, and would require adding code for checking that header at layouts.* at https://github.com/wekan/wekan/tree/devel/client/components/main login page and/or wekan/server/authentication.js and then based on that check it similarly with Javascript code and login user, with similar code like in that PHP extension. Anyway, that extension PHP code is not long, and everything required to login is already in Wekan LDAP code, so this would require just someone that has time to look at the code, figure it out and add PR.

adrienaury commented 5 years ago

Hi! Do you have an update on this feature request ? I'm also interrested, same use case as @perlhub.

xet7 commented 5 years ago

@adrienaury

Not yet. Do you have time to help?

adrienaury commented 5 years ago

Not for contributing to the code, but anything that can help you (test version, paste logs, ...). Thanks for your time @xet7

adrienaury commented 5 years ago

I gave a look at the code to see if I could develop it rapidly, but it seems to be impossible to read custom HTTP Headers from the Meteor server side. I'm not very familiar with Meteor, and I looked the entire web for a few hours without solution.

The problem is that Meteor filters HTTP Header based on a whitelist, and it's very obscure to me, and not documented.

Aside from this, I think I know how to implement it.

xet7 commented 5 years ago

@adrienaury

How would you implement this?

Is there existing serverside implementation in some other programming language? For example Javascript or PHP?

I'm currently researching how to make non-Meteor version of Wekan.

xet7 commented 5 years ago

Doh, now I noticed above the link to MediaWiki example code. I'll look at it.

xet7 commented 5 years ago

@adrienaury

Is this Kanboard feature also about this Client-Side (Mutual) Authentication issue? https://docs.kanboard.org/en/latest/admin_guide/reverse_proxy_authentication.html

Both Wekan and Kanboard have MIT license. There is differences in web UI and other features.

adrienaury commented 5 years ago

How would I implement it

So as I said I'm new to Meteor and maybe it's not the best way to do it, but I tested it with hard coded values and it works as expected.

In header-login.js, I created a new Authentication Handler :

Accounts.registerLoginHandler("headers", function(loginRequest) {
  if (!loginRequest.checkHeaders) {
    return undefined;
  }
  console.log("Handling login from headers");
  console.log(loginRequest);

  // HERE IS THE PROBLEM : we need to find a way to get the request headers to get values of loginId, loginEmail, loginFirstname and loginLastname.
  var sessionData = this.connection || (this._session ? this._session.sessionData : this._sessionData);
  // only a few headers are visible because of the whitelist filtering
  console.log(this);
  console.log(sessionData);

  // so i used fixed values for testing
 var loginId = "user";
 var loginEmail = "user@domain.com";
 var loginFirstname = "User";
 var loginLastname = "User";

  const serviceName = 'headers';
  const serviceData = {};
  const serviceOptions = {
    profile: {}
  };
  serviceData.id = loginId; // unique
  const result = Accounts.updateOrCreateUserFromExternalService(
    serviceName,
    serviceData,
    serviceOptions
  );
  if (!result || !result.userId) {
    throw new Meteor.Error('someError', 'Some message');
  }
  // update username if you have one
  Accounts.setUsername(result.userId, loginId);
  // add email (not verified)
  Accounts.addEmail(result.userId, loginEmail);

  var stampedToken = Accounts._generateStampedLoginToken();
  var hashStampedToken = Accounts._hashStampedToken(stampedToken);
  const user = Users.findOne({username: loginId});
  if (user) {
    console.log("Found user !!");
    Meteor.users.update(user._id, {
      $push: {
        'services.resume.loginTokens': hashStampedToken
      }
    });
    console.log("Everything OK !!");
    return {
      userId: user._id,
      token: stampedToken.token
    };
  }
  console.log("No user");
  return undefined;
});

And then in layout.js I added this (didnt know where to insert it, so I put it inside Template.userFormsLayout.onCreated and it seems OK.

   Accounts.callLoginMethod({
    methodArguments: [{checkHeaders: true}],
    userCallback(error, result) {
      console.log("callLoginMethod returned!")
      if (error) {
        console.error(error);
      }
      else {
        console.log("User logged");
        FlowRouter.go('/')
      }
    },
  });
});
adrienaury commented 5 years ago

@adrienaury

Is this Kanboard feature also about this Client-Side (Mutual) Authentication issue? https://docs.kanboard.org/en/latest/admin_guide/reverse_proxy_authentication.html

Both Wekan and Kanboard have MIT license. There is differences in web UI and other features.

Yes exactly !

xet7 commented 5 years ago

@adrienaury

Did you find yet where header whitelist is?

I'm not sure could it be related to browser-policy? https://github.com/wekan/wekan/blob/devel/server/policy.js https://atmospherejs.com/meteor/browser-policy

This is how Wekan serverside sets some CORS headers: https://github.com/wekan/wekan/blob/devel/server/cors.js

There is also not yet merged PR for adding more CORS headers: https://github.com/wekan/wekan/pull/2429

For header login another issue: https://github.com/wekan/wekan/issues/2019 I originally tried this non-working code: https://github.com/wekan/wekan/commit/08db39d76a2454cdc42c225597863e982ca77e82

This maybe is how in plain javascript headers are read: https://stackoverflow.com/questions/220231/accessing-the-web-pages-http-headers-in-javascript

Does any of this give you ideas how to get it working?

xet7 commented 5 years ago

@adrienaury

In that header login issue, here are how I planned to do it, that's the explanation of that non-working code: https://github.com/wekan/wekan/issues/2019#issuecomment-499502369

adrienaury commented 5 years ago

Did you find yet where header whitelist is?

No, I checked official online documentation/forums and some Stackoverflow posts but none said where it is located and how (if possible) to configure it.

I found some piece of info here : https://www.phusionpassenger.com/library/indepth/meteor/secure_http_headers.html

Meteor uses sockjs behind the scenes to transform requests into connection objects, and sockjs enforces a whitelist on headers, so using connection.httpHeaders won't work to access our secure headers from meteor.

I'm not sure could it be related to browser-policy?

No idea, I need to research more :(

Does any of this give you ideas how to get it working?

I tried some solution base on WebApp.rawConnectHandlers and yes it was possible to read HTTP Headers without limitation with this solution. But I set it aside because it was difficult to make it works with Accounts.registerLoginHandler which I thought is the proper way (maybe I'm wrong ?).

In that header login issue, here are how I planned to do it, that's the explanation of that non-working code

I'm aware of the other issue (#2019), actually I first landed on this issue when I started to dig it. For me this is a duplicate of this current issue we're discuting. I was planning to use the constants you already added in the code to get the correct headers. I think you can stille use HEADER_LOGIN_ID by the way, in my opinion HEADER_LOGIN_ID should match first, then check email with HEADER_LOGIN_EMAIL and if not match, give an option to update the user account ?

I originally tried this non-working code

I will look into it to see if I can be inspired :)

xet7 commented 5 years ago

I tried some solution base on WebApp.rawConnectHandlers and yes it was possible to read HTTP Headers without limitation with this solution. But I set it aside because it was difficult to make it works with Accounts.registerLoginHandler which I thought is the proper way (maybe I'm wrong ?).

Whoa, it was possible to read HTTP Headers without limitation? Then of course try to use it, to get it working. I did not even get that far that you did here.

adrienaury commented 5 years ago

@xet7 I'll try to continue tomorrow (it's evening here in France) and I keep you updated.

xet7 commented 5 years ago

Thanks! That issue wekan/wekan#2019 has links to all commits I added when I tried to implement it.

xet7 commented 5 years ago

How it works

From https://www.phusionpassenger.com/library/indepth/meteor/secure_http_headers.html

An HTTP header is considered secure if it begins with !~.
If the client-sent HTTP request contains any headers that begin with !~,
then Passenger will reject that request.
This prevents the client from spoofing any secure headers.

Only Passenger may send secure headers to the application.

Comments by xet7

This above description is so clear and simple about how this works.

It's not a problem that we use code to read all raw headers directly.

In front of Wekan is the webserver (Caddy/Nginx/Apache/Siteminder/etc) that will drop extra headers that are coming from client, preventing header spoofing. Then that webserver (Caddy/Nginx/Apache/Siteminder/etc) adds it's own allowed headers. Then in Wekan is defined what header names to read, and autologins to Wekan.

adrienaury commented 5 years ago

Sorry I wasn't able to make it work..

With WebApp.rawConnectHandlers, I can read the headers, BUT I can't interact with Accounts and Users. Any calls to callLoginMethod or Users.find, fails.

A possible implementation would be to store the headers value from WebApp.rawConnectHandlers when it is accessible, and use them later. I tried to access some sort of Session storage, server side but private to the client making the request. But I didn't find anything working when the user is not yet logged...

perlhub commented 5 years ago

Thanks for looking into this guys. It sounds like you were able to fetch the HTTP headers, but not auto-login with them.

I logged in to post another example link I found: https://grafana.com/docs/auth/auth-proxy/. It shows how Grafana does what we're trying to do.

Behind my Apache reverse proxy I have Grafana and Wekan running. Apache logs me in, then forwards a custom header (i.e. X-WEBAUTH-USER) to Grafana and Wekan. Grafana then logs me in with the header username. It would be great if Wekan did this too.

xet7 commented 5 years ago

@perlhub

So does Grafana require only username in header to login? Is username same as e-mail address? Does it not use firstname and lastname at all?

xet7 commented 5 years ago

I think for Wekan, only required header would probably be username (like nickname or email address). Having firstname, lastname, fullname, gravatar URL etc would be optional.

perlhub commented 5 years ago

@xet7 Yea, only the username is required. But you can add additional headers. This is in their config file:

# Optionally define more headers to sync other user attributes # Example headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL headers =