Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client-Side (Mutual) Authentication #59

Open
perlhub opened this issue Feb 17, 2019 · 24 comments
Open

Client-Side (Mutual) Authentication #59

perlhub opened this issue Feb 17, 2019 · 24 comments

Comments

@perlhub
Copy link

perlhub commented Feb 17, 2019

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
Copy link
Member

xet7 commented Feb 17, 2019

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
Copy link
Author

perlhub commented Feb 17, 2019

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
Copy link
Member

xet7 commented Feb 17, 2019

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
Copy link

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

@xet7
Copy link
Member

xet7 commented Jun 3, 2019

@adrienaury

Not yet. Do you have time to help?

@adrienaury
Copy link

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

@adrienaury
Copy link

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
Copy link
Member

xet7 commented Jun 8, 2019

@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
Copy link
Member

xet7 commented Jun 8, 2019

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

@xet7
Copy link
Member

xet7 commented Jun 8, 2019

@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
Copy link

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 = "[email protected]";
 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
Copy link

@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
Copy link
Member

xet7 commented Jun 8, 2019

@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:
wekan/wekan#2429

For header login another issue:
wekan/wekan#2019
I originally tried this non-working code:
wekan/wekan@08db39d

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
Copy link
Member

xet7 commented Jun 8, 2019

@adrienaury

In that header login issue, here are how I planned to do it, that's the explanation of that non-working code:
wekan/wekan#2019 (comment)

@adrienaury
Copy link

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
Copy link
Member

xet7 commented Jun 8, 2019

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
Copy link

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

@xet7
Copy link
Member

xet7 commented Jun 8, 2019

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

@xet7
Copy link
Member

xet7 commented Jun 8, 2019

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
Copy link

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
Copy link
Author

perlhub commented Jun 23, 2019

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
Copy link
Member

xet7 commented Jun 25, 2019

@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
Copy link
Member

xet7 commented Jun 25, 2019

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
Copy link
Author

perlhub commented Jun 30, 2019

@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 =

@xet7 xet7 transferred this issue from wekan/wekan Jan 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants