I'm using openresty nginx v1.11.2.4. I wish to be able to authenticate users before they are given access to a resource or before they try to PUT something on the server. I am using the http_auth_request_module and the following is an except from my nginx.conf file:
location /video/ {
auth_request /auth;
root /usr/local/openresty/nginx/html;
}
location = /auth {
more_set_headers "WWW-Authenticate: Basic";
return 401;
}
This results in the browser asking for user credentials alright but now how do I get/process the user credentials from the client?
The ngx_http_auth_request_module implements client authorization based on the result of a subrequest.
If you want to use basic authentication you don't need to use ngx_http_auth_request_module. Use http://nginx.org/en/docs/http/ngx_http_auth_basic_module.html
following the answer to the question here: Nginx authentication with auth_request module
I was able to process the username and password by accessing the $http_authorization variable in my nginx.conf file. The following is an excerpt from my nginx.conf:
location /video {
satisfy any;
auth_basic "Private Property";
auth_basic_user_file /usr/local/openresty/nginx/conf/htpasswd;
auth_request /auth;
root /usr/local/openresty/nginx/html;
client_max_body_size 1000M;
if ($request_method != "GET"){
content_by_lua_file /root/Documents/contentbylua.lua;
}
}
location = /auth {
set $authHeader $http_authorization;
set $authUName $remote_user;
content_by_lua_file /root/Documents/restrict.lua;
}
The following conf allows me to authenticate a user whose credentials are stored in a redisDB in the restrict.lua file which returns a 200 or 401 code depending on the credentials of the user back to the /location block.
The response (username & password) is accessed in the restrict.lua file by ngx.var.authHeader. Some string processing is done to remove the 'Basic' then the remnant is base64 decoded and then some string processing is done on it to obtain the password. That's all
Related
I need to send some basic auth credentials (es. user:pass) to nginx in the form of query parameter (es. http://example.com?BASIC_AUTH=dXNlcjpwYXNz) and being able to forward them in the more usual Authorization: Basic dXNlcjpwYXNz header form to a target server behind the proxy.
I'm already able to retrieve the value of the encoded auth string with a regular expression. The problem is that very often that value may contain some character that need to be percent-encoded in the URL. Es. user:pass! -> ?BASIC_AUTH=dXNlcjpwYXNzIQ== becomes ?BASIC_AUTH=dXNlcjpwYXNzIQ%3D%3D
Therefore, when I forward the request to the target server, I end up specifing Authorization: Basic dXNlcjpwYXNzIQ%3D%3D which the target server will reject, giving a 401 Unauthorized.
How can I force nginx to decode the auth string before setting the Authorization header? Thanks in advance for your help.
Note: I can't send the auth string in the Authorization header in the first place due to some application-specific constraints.
"Pure" nginx solution
Unfortunately nginx does not provide a rich string operations set. I think there isn't a way to do global search-and-replace through some string (which can be a solution if we could replace all %2B with +, %2F with / and %3D with =). However there are circumstances under which nginx performs an urldecoding of some string - when this string becomes a part of an URI which will be forwarded to an upstream proxy server.
So we can add a value of a BASIC_AUTH request argument to the URI and make a proxy request to ourself:
# Main server block
server {
listen 80 default_server;
...
location / {
if ($arg_basic_auth) {
# "basic_auth" request argument is present,
# append "/decode_basic_auth/<BASE64_token>" to the URI
# and go to the next location block
rewrite ^(.*)$ /decode_basic_auth/$arg_basic_auth$1 last;
}
# No "basic_auth" request argument present,
# can do a proxy call from here without setting authorization headers
...
}
location /decode_basic_auth/ {
# This will be an internal location only
internal;
# Remove "basic_auth" request argument from the list of arguments
if ($args ~* (.*)(^|&)basic_auth=[^&]*(\2|$)&?(.*)) {
set $args $1$3$4;
}
# Some hostname for processing proxy subrequests
proxy_set_header Host internal.basic.auth.localhost;
# Do a subrequest to ourselfs, preserving other request arguments
proxy_pass http://127.0.0.1$uri$is_args$args;
}
}
# Additional server block for proxy subrequests processing
server {
listen 80;
server_name internal.basic.auth.localhost;
# Got URI in form "/decode_basic_auth/<BASE64_token>/<Original_URI>"
location ~ ^/decode_basic_auth/([^/]+)(/.*)$ {
proxy_set_header Authorization "Basic $1";
# Setup other HTTP headers here
...
proxy_pass http://<upstream_server>$2$is_args$args;
}
# Do not serve other requests
location / {
return 444;
}
}
Maybe this is not a very elegant solution, but it is tested and works.
OpenResty / ngx_http_lua_module
This can be easily solved with openresty or ngx_http_lua_module using ngx.escape_uri function:
server {
listen 80 default_server;
...
location / {
set $auth $arg_basic_auth;
if ($args ~* (.*)(^|&)basic_auth=[^&]*(\2|$)&?(.*)) {
set $args $1$3$4;
}
rewrite_by_lua_block {
ngx.var.auth = ngx.unescape_uri(ngx.var.auth)
}
proxy_set_header Authorization "Basic $auth";
# Setup other HTTP headers here
...
proxy_pass http://<upstream_server>;
}
}
I'm evaluating nginx to act as rate limiter for a multi tenancy REST API system. I need to limit API calls by tenant-id.
For example i want to allow 100 r/s for tenant1 and only 50 r/s for tenant2.
It can be easily achived when there are differant urls like: "me.com/tenant1/api" and "me.com/tenant2/api" (with the location directive).
But, in my case the urls are the same for all tenants "me.com/api" (I can't change this).
To find the tenant-id I need to extract a JSON attribute from the Body of the request, and then check the DB for the real tenant-id.
Is it possible to limit_req with my requirements?
Thank for the help!
I decided to build another service getTenant for parsing the body and extracting the Tenant from the DB. This service is called internally by Nginx.
I'm not sure if that is the best nginx (/openresty) solution, but this is what i came up with:
limit_req_zone t1Limit zone=t1Zone:10m rate=200r/s;
limit_req_zone t2Limit zone=t2Zone:10m rate=90r/s;
server {
location /api{
content_by_lua_block {
ngx.req.read_body();
local reqBody = ngx.req.get_body_data()
local res = ngx.location.capture("/getTenant", {method=ngx.HTTP_POST,body=reqBody});
local tenantId= res.body;
if tenantId== "none" then
ngx.log(ngx.ERR, "Tenant not found!");
ngx.say(tenantId);
else
ngx.req.set_header("x_myTenantId", tenantId)
local res2 = ngx.location.capture("/" .. tenantId .."/doApi", {method=ngx.HTTP_POST,body=reqBody});
if res2.status == ngx.HTTP_OK then
ngx.say(res2.body);
ngx.exit(res2.status);
else
ngx.status = res2.status
ngx.exit(res2.status)
end
end;
}
}
location /getTenant {
internal; #this is not accessible from outside.
proxy_pass http://UpStream1/getCustomer;
proxy_set_header X-Original-URI $request_uri;
}
location /tenant1/doApi {
internal; #this is not accessible from outside.
# Proxy all requests to the AReqUpStream server group
proxy_pass http://UpStream2/doApi;
limit_req zone=tenant1Zone burst=25;
limit_req_log_level notice;
}
location /tenant2/doApi {
internal; #this is not accessible from outside.
# Proxy all requests to the AReqUpStream server group
proxy_pass http://UpStream2/doApi;
limit_req zone=tenant2Zone burst=10 ;#nodelay;
limit_req_status 409;
limit_req_log_level notice;
}
}
Basically, when me.com/api is called, a new subrequest is issued to service /getTenant. The response of that call is used to build another subrequest call to the /tenant[X]/doApi service. That way i can define locations per tenant and provide different rate_limis to each.
Comments on that are more than welcome!
I'm trying to secure my webapp by using nginx base authentication.
What I'm looking for is a way to force the browser to show my custom html login page instead of the default login popup but still handle the authorization process.
I try to omit the 'WWW-Authenticate' header and the popup wasn't display but I've no idea how to force the browser to add the 'Authorization' header for each request.
hereby nginx.conf:
location /{
auth_basic "Restricted";
auth_basic_user_file htpasswd;
proxy_pass http://tomcat:8080/;
error_page 401 /login.html;
}
location = /login.html {
root html;
more_clear_headers 'WWW-Authenticate';
}
You need to add proxy_intercept_errors on; or you will not be able to define error_page. Otherwise NGINX just passes the HTTP 401 response back to the client.
location /{
auth_basic "Restricted";
auth_basic_user_file htpasswd;
proxy_pass http://tomcat:8080/;
proxy_intercept_errors on;
error_page 401 /login.html;
}
location = /login.html {
root html;
more_clear_headers 'WWW-Authenticate';
}
Syntax: proxy_intercept_errors on | off;
Default: proxy_intercept_errors off;
Context: http, server, location
Determines whether proxied responses with codes greater than or equal to 300 should be passed to a client or be intercepted and redirected to nginx for processing with the error_page directive.
*has* to be a better way to accomplish this, but i managed to do it with X-Accel-Redirect and php, here's how:
i wanted to have a custom login page for folder /foo/ (and recursively all its content)
... first i renamed the on-disk folder /var/www/html/foo to /var/www/html/internal_foo , then i added to nginx config:
location ~ /internal_foo {
internal;
# even if this is the default root, the internal-directive seems to prevent inheriting the root directive, so have to explicitly specify it here...
root /var/www/html;
}
location ~ /foo {
try_files "" /foo.php$is_args$args;
}
then i created a /var/www/html/foo.php with the contents:
<?php
// warning: script vulnerable to timing attack which can disclose existence of files. (realpath() is not constant-time)
if(is_authorised()){
$translated="/internal_foo/".urldecode(substr($_SERVER['REQUEST_URI'],strlen("/foo/")));
$file=__DIR__.$translated;
$file=realpath($file);
if(false===$file){http_response_code(404);exit();}
if(0!==strpos($file,__DIR__.DIRECTORY_SEPARATOR."internal_foo")){
// probably a hacker attempting ../../../etc/passwd schenanigans.
http_response_code(400);
exit();
}
header("X-accel-redirect: ".$translated);
}else{
show_loginpage();
}
and voila, if a user is authorized, the request is sent to nginx, otherwise show_loginpage(); is executed (where you can put your custom login page), mission accomplished :)
If you really want to do this. The easiest way is to put the username and password into the url. Basic Authentication supports this. Change all requests to the format below and it will work:
"http://" + username + ":" + password + "#example.com"
I'm trying to get access to media files (images, videos) sitting behind an OAuth2 authentication.
In order to access the resource I need to add a custom Authorization Bearer token to the request, so I can't use a simple rewrite (well, as far as I know at least).
It cannot be done via plain HTML (say img or video tag) so I'm considering to have Nginx proxying the queries to the final server.
Each of the media resources would be loaded via a /proxy path, with a token parameter (for authentication) and url for the actual resource to load.
Sample URL:
http://myserver.com/proxy/?token=12345&url=http://protectedserver.com/custompath/asset
This is what I came up with but I am not quite sure how to configure the proxy_pass directive since I need it to proxy to the $url variable specifically. I do not need to proxy the path (which would be empty anyway).
location /proxy/ {
if ($arg_token ~ "^$") { return 404; }
if ($arg_url ~ "^$") { return 404; }
set $url $arg_url;
proxy_set_header Authorization "Bearer $arg_token";
set $args "";
#proxy_pass $url;
}
Note: this will be run in a closed environment and only specific machines (kiosks with limited interaction) will be able to access the page so I'm not concerned about a potential leak of the auth token.
I noticed a similar question on ServerFault, but no one had an answer to that:
https://serverfault.com/questions/671991/nginx-proxy-pass-url-from-get-argument
I'm looking for a config setting to make it work or a viable alternative solution.
Here is a correct configuration for my problem:
location /proxy/ {
if ($arg_token ~ "^$") { return 404; }
if ($arg_url ~ "^$") { return 404; }
set $url $arg_url;
set $token $arg_token;
set $args "";
# IMPORTANT, this is required when using dynamic proxy pass
# You can alternatively use any DNS resolver under your control
resolver 8.8.8.8;
proxy_pass $url;
proxy_set_header Authorization "Bearer $token";
proxy_redirect off;
}
I setup NGINX on CentOS and set config file to prevent direct access video file.
This is example code that I use.
location /videos/ {
valid_referers none blocked abccc.com;
if ($invalid_referer) {
return 403;
}
root /var/cdn/videos/;
## This must match the URI part related to the MD5 hash and expiration time.
secure_link $arg_st,$arg_e;
## The MD5 hash is built from our secret token, the URI($path in PHP) and our expiration time.
secure_link_md5 Gf1Pv52EifS1f8G9agn2k2n8a$uri$arg_e;
## If the hash is incorrect then $secure_link is a null string.
if ($secure_link = "") {
return 403;
}
## The current local time is greater than the specified expiration time.
if ($secure_link = "0") {
return 403;
}
gzip off;
gzip_static off;
mp4;
flv;
mp4_buffer_size 5M;
mp4_max_buffer_size 10M;
#mp4_limit_rate on;
#mp4_limit_rate_after 30s;
## If everything is ok $secure_link is 1.
rewrite ^/videos/(.*)$ /videos/$1 break;
}
But when restart server and test it, I still access video file in folder via
direct url and secure link.
Other wise when I try to use this configuration in localhost, it work perfectly.
What's I do wrong?