Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional nginx auth_request

I want to have my nginx proxy perform a subrequest for authentication only if the client is not already authenticated. The conditional part is where I am stuck. How can I craft a configuration so that the client is only authenticated once per session?

I am able to successfully perform an auth_request to Apache and pull back the headers I want to pass on to the back-end, but this is occurring on every request and is expensive.

In the example here, my goal is to only perform the auth_request if the "Authorization" header is missing or empty or alternately a cookie containing the token

# DEFAULT BACKEND
    location / {

        proxy_pass_request_body off;

        if ($http_authorization ~* '')
        {
            rewrite ^(.*)$ /__login;
        }

        if ($user !~* "([aa-zZ]+)@example.com")
        {

        }

        if ($http_cookie !~* "(auth_cookie=([aa-zZ]+)@example.com)")
        {
            add_header Set-Cookie "auth_cookie=$user;domain=.example.com;Max-Age=3000";

        }

        proxy_pass_header x-webauth-user;
        proxy_pass_header Set-Cookie;
        proxy_pass http://example.com:6762/;

   }

location /__login { internal;

    auth_request /auth;
    auth_request_set $user $upstream_http_x_webauth_user;
    set $xuser $user;

    add_header Auth-User $user;
    proxy_set_header User-Name $user;
    proxy_set_header Authorization $http_authorization;

    #proxy_pass_header x-webauth-user;
    #proxy_pass_header Set-Cookie;

    proxy_pass http://example:6762/;

    access_log /etc/nginx/login_debug.log;
   }


location = /auth{
    internal;
    proxy_pass http://example.com:81/;

    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
    #proxy_pass_header  Set-Cookie;
    #proxy_pass_header  x-webauth-user;
}

The Auth-User header gets lost on all requests after the first and the cookie never seems to get set, beyond that the page doesn't actually seem to render in a browser. I am obviously doing something very wrong, could some please help me figure this out.

like image 586
Baywatch Avatar asked Nov 04 '16 21:11

Baywatch


2 Answers

Please check out the NJS (https://nginx.org/en/docs/njs/) module. It's really simple and for sure can do what you want. Here is the example solution:

file: /etc/nginx/conf.d/default.conf:

server {
    listen 80;
    server_name "SOME_SERVER";
    # make an authentication subrequest for every request
    auth_request /auth;

    # create a new variable AuthToken and set its value to the res.SOMEVALUE from the later subrequest... 
    auth_request_set $AuthToken $sent_http_token;
    
    # add new AuthToken to the request
    proxy_set_header Authorization $AuthToken;

    location / {
        proxy_pass http://SOME_ENDPOINT;
    }

    location = /auth {
        internal;

        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;

        js_content auth.main;
    }

    location /get-new-token-location {
       internal;

       proxy_pass http://SOMEURL;
    }
}

and the example of nginx.conf file to show how to enable the NJS module:

...
pid /var/run/nginx.pid;

load_module /usr/lib/nginx/modules/ngx_http_js_module.so;

events {
    use epoll;
    worker_connections 10000;
}


http {
    # import njs scripts
    js_import auth from /path/to/the/auth.js;
    
    include /etc/nginx/conf.d/default.conf;
}

and finally, the main function from auth.js file:

export default {main}

function main(r) {
    var token = "";
    // search token in Authorization header
    if (this.requestHeaderExists(r, 'Authorization')) {
        var m = r.headersIn.Authorization.match(/Bearer\s+(.+)/);

        if (m !== null && typeof m[1] !== 'undefined') {
            token = m[1];
        }
    }

    // search token in cookie
    if (token.length == 0) {
       ... code here ...
    }

    // token was found, you can somehow validate it if you want
    if (token.length > 0) {
       .., make sure token is valid...
    }
    else { // there is no token, so ask for the new one
       r.subrequest('/get-new-token-location, { method: 'GET' }, function(reply) {  
         var res = JSON.parse(reply.responseBody);
         // add token to the response headers of this sub-request
         r.headersOut['token'] = res.SOMEVALUE;
       }
    }
   r.return(200);
   return;
}

Please treat it as an example. Ok, maybe it looks complicated, but it is really powerful and for sure you can find more examples in the world wide web.

like image 80
Piotr Krawcow Avatar answered Sep 23 '22 02:09

Piotr Krawcow


The Nginx wiki warns that if inside location may give unexpected results, but that rewrite ... last; is safe. Here is an example:

location / {
    if ($cookie_UserName = "") {
        rewrite ^ /__login$uri last;
    }
    proxy_pass http://backend-app;
}
location /__login {  internal;
    rewrite ^/__login(?<realurl>/.*)$ $realurl break;
    auth_request /auth;
    auth_request_set $user $upstream_http_x_webauth_user;
    proxy_set_header Cookie UserName=$user;
    proxy_pass http://backend-app;
    add_header Set-Cookie "UserName=$user;Max-Age=300";
}
location = /auth {  internal;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_pass http://auth-server/validate;
}

There are two cases: Cookie:UserName exists or not. If it exists the first proxy_pass is executed. Otherwise /__login is used. Note that $uri is passed, so that it can be sent to backend-app.

For more advanced conditionals, you may use map instead of if.

Beware, though, that not authenticating every request runs the risk of accepting requests with a "faked" cookie/header.

like image 25
Richard Tingstad Avatar answered Sep 23 '22 02:09

Richard Tingstad