Skip to content
New issue

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

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

Already on GitHub? # to your account

Add option to retrieve username from HTTP header #219

Merged
merged 5 commits into from
May 22, 2019

Conversation

andrewheberle
Copy link
Contributor

@andrewheberle andrewheberle commented May 22, 2019

This PR extends the IP based identification to allow the username to be extracted from a specific HTTP request header defined in the server config as follows:

{
  ...
  "access": {
    "allowed_users": [ ... ],
    "admin_users": [ ... ],
    "user_header_name": "X-Auth-Name"
  }
}

This header is only used if the request comes from a trusted IP.

If the configured header does not exist, the identification falls back to the IP based method.

The use case for this, in our case, is an instance of script-server that sits behind a reverse proxy (HAProxy in this case) that does authentication itself, then adds various headers to the request (username, email address, display name etc).

If the connection comes from a trusted ip, the username is pulled from a configured header
@andrewheberle
Copy link
Contributor Author

OK, it looks like this change breaks a bunch of the tests in Travis CI.

I'll see what I can do to resolve this and update the PR.

Update identification tests to handle new IpBasedIdentification args

TODO: Write tests for new functionality
@bugy bugy added the feature label May 22, 2019
@bugy
Copy link
Owner

bugy commented May 22, 2019

Hi @andrewheberle, great thanks for the contribution!
Nice feature and implementation

@bugy bugy merged commit 409f766 into bugy:master May 22, 2019
@andrewheberle andrewheberle deleted the patch-2 branch May 22, 2019 06:38
@muzzol
Copy link

muzzol commented May 22, 2019

The use case for this, in our case, is an instance of script-server that sits behind a reverse proxy (HAProxy in this case) that does authentication itself, then adds various headers to the request (username, email address, display name etc).

hi @andrewheberle , could you please provide an example configuration for HAProxy?

we are testing it on our company but we're not really experienced (we usually use apache proxypass).

@andrewheberle
Copy link
Contributor Author

Hi @muzzol

There are a few parts to this which are:

  1. HAProxy with LUA support compiled in - some distro builds omit this
  2. A slightly modified version of the orignal "sub-request" HAProxy LUA script here: https://github.com/TimWolla/haproxy-auth-request - our version: https://github.com/andrewheberle/haproxy-auth-request
  3. A service to actually handle the sub-request and authenticate the user - In our case this is a simple SAML 2.0 service provider - code is here: https://gitlab.com/andrewheberle/go-http-auth-sso

Could I also mention this is not really battle tested...we use it internally for our small team (approx 30 users) and it works fine, but to be honest I am sure someone with more LUA scripting and Go experience will probably cringe at the changes made to "haproxy-auth-request" and my "go-http-auth-sso" daemon...but moving on...

The config we use in HAProxy is as follows (some edits for privacy/brevity):

global
    log         stdout format short daemon

    chroot      /var/lib/haproxy
    pidfile     ${PIDFILE}
    maxconn     4000
    user        haproxy
    group       haproxy

    nbthread    1

    # turn on stats unix socket
    stats socket ${SOCK} level admin expose-fd listeners
    stats timeout 2m

    # Modern (secure) profile from - https://mozilla.github.io/server-side-tls/ssl-config-generator/
    ssl-default-bind-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
    ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
    ssl-default-server-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
    ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
    tune.ssl.cachesize 262144
    tune.ssl.lifetime 3600

    # Load LUA script for authentication
    lua-load /etc/haproxy/auth-request.lua

defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option                  forwardfor
    option                  redispatch
    http-reuse              safe
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    timeout tunnel          1h
    maxconn                 4000

frontend main
    bind 192.168.101.96:80
    bind 192.168.101.96:443 ssl crt /etc/haproxy/ssl/server.pem

    # Script Server
    acl dom_script_server hdr(host) -i script-server.example.local

    # go-http-auth-sso
    acl dom_sp hdr(host) -i sso.example.local
    acl url_sp path_beg -i /saml
    acl url_sp path_beg -i /auth/#

    # do http auth request for matching paths/urls
    http-request lua.auth-request auth_request /auth/check if dom_script_server
    # redirect to SP if response is 401
    http-request redirect code 302 location https://sso.example.local/auth/#?returnurl=https://%[hdr(host)]%[capture.req.uri] if { var(txn.auth_response_code) 401 } dom_script_server
    # deny the request if ! auth_response_successful from the sub-request
    http-request deny if ! { var(txn.auth_response_successful) -m bool } dom_script_server
    # Add "X-Auth-..." headers if the data has been returned from sub-request
    http-request set-header X-Auth-Name %[var(txn.auth_response_name)] if { var(txn.auth_response_name) -m found }

    # Use backend based on ACLs
    use_backend script-server if dom_script_server
    use_backend sp_https if dom_sp url_sp

    # Default to a silent drop
    default_backend drop

frontend http_auth
    # http_auth front end so we can add the required Host header to the request
    # only accepts requests for "/auth/check"
    bind 127.0.0.1:8001
    acl url_auth_check path -i /auth/check
    http-request set-header Host sso.example.local
    use_backend sp_https if url_auth_check
    default_backend drop

backend auth_request
    # back-end used for the auth sub-request
    # server is http_auth front-end above
    server http_auth 127.0.0.1:8001 check

backend sp_https
    # SAML 2.0 SP used for auth requests
    option log-health-checks
    option httpchk GET /healthz
    server http-auth 127.0.0.1:8443 check ssl ca-file /etc/haproxy/ca.pem

backend script-server
    # script-server back end
    # users connect to: https://script-server.example.local/
    # script server is running on: http://127.0.0.1:5000
    # redirect to https
    http-request redirect scheme https if !{ ssl_fc }
    # equivalent of apache ProxyPassReverse to rewrite any redirects from the back end
    acl is_redirect res.hdr(Location) -m found
    http-response replace-value Location ^(http|https)://(.*) https://\2 if is_redirect
    server script-server 127.0.0.1:5000 check

backend drop
    # silent drop back end
    http-request silent-drop

The basic flow of the authentication process is as follows:

  1. Request comes in from the user which matches the "dom_script_server" ACL
  2. The "http-request lua.auth-request ..." line executes the LUA function in the "/etc/haproxy/auth-request.lua" script
  3. This script does a HTTP request to "/auth/check" against the specified back end ("auth_request")
  4. This HTTP request to that back-end is actually against another HAProxy front-end ("http_auth") as our particular authentication enforces SSL connections only and requires a specific "Host" header...so this is then handed to the real back-end ("sp_https")
  5. After all that (this is still in the LUA script), the script returns the status code from the HTTP request, plus some other variables depending on the response too. The auth service responds to "/auth/check" with some JSON that contains SAML "claims" (fancy SAML jargon for user infomation), which the script uses to map to HAProxy variables that can be used later.
  6. Based on the response code there are some "http-request" directives to either redirect the user to the login page if the HTTP response was a 401, deny the request if auth was unsuccessful, or set a header containing the "username" variable ("var(txn.auth_response_name)") that was set via the LUA script.
  7. After all that, we finally get to actually sending the real user request somewhere, so based on the "dom_script_server" ACL match the request is sent to the "script-server" backend, which is Script Server itself listening on 127.0.0.1:5000
  8. Based on the changes in this PR, script server is configured to listen only on 127.0.0.1 so the only access for users is via HAProxy, with 127.0.0.1 set as a trusted ip, and "X-Auth-Name" set as the "user_header_name" option.

There is quite a lot going on in the above and it took a fair amount of tinkering to get right...and I probably haven't explained things very well either.

SAML 2.0 Service Provider (in Golang): https://gitlab.com/andrewheberle/go-http-auth-sso
Customised version of "auth-request.lua" script: https://github.com/andrewheberle/haproxy-auth-request
HAProxy: http://www.haproxy.org/download/1.9/src/haproxy-1.9.8.tar.gz (we compile version of v1.9 locally as our chosen distro doesn't include LUA support in its standard packages)

Example JSON response from our service from an authenticated user:

{
  "status": "logged in",
  "data": {
    "displayname": [
      "Guy Incognito"
    ],
    "emailaddress": [
      "guy.incognito@example.local"
    ],
    "givenname": [
      "Guy"
    ],
    "name": [
      "guy.incognito@example.local"
    ],
    "surname": [
      "Incognito"
    ]
  }
}

The map file that is used in the LUA script to map the above JSON data to HAProxy variables:

# claim      variable
name         txn.auth_response_name
displayname  txn.auth_response_displayname
givenname    txn.auth_response_givenname
surname      txn.auth_response_surname
emailaddress txn.auth_response_emailaddress

The mapping of "claims" to "variables" above is then what is used in the "http-request set-header" lines in the "main" frontend.

Hoping this all makes some sort of sense.

@bugy
Copy link
Owner

bugy commented May 23, 2019

Hi @muzzol, for apache proxy I've found this question:
https://serverfault.com/questions/193458/pass-username-from-apache-basic-authentication-to-cherrypy

May be you could give it a try.

@andrewheberle
Copy link
Contributor Author

The equivalent option in HAproxy to extract the username from any basic auth you have done via the reverse proxy would be as follows:

  1. Define a userlist
userlist myusers
    user user1 insecure-password password1
    user user2 insecure-password password2
  1. Require authentication and set the header based on the auth results in the frontend
    acl auth_ok http_auth(myusers)

    # Require auth
    http-request auth unless auth_ok 

    # Set the header based on basic auth
    http-request set-header X-Auth-Name %[http_auth_group(myusers)]

My overly complex example is how we are delegating authentication to another service, which is a SAML 2.0 service provider that then authenticates users against Azure AD, rather than using the authentication built into HAProxy, which is limited to just basic auth.

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants