From bde9522300fe5f1eb7513e7f2ee6731958987c23 Mon Sep 17 00:00:00 2001 From: "Randall A. Gordon" Date: Wed, 3 Apr 2019 06:59:07 -0700 Subject: [PATCH] Basic Auth Configuration (#45) * Basic Basic Auth * Allow username and password to be passed in via env vars * Add test spec for basic auth returning 401 * Update README with Basic Auth config info * Add .htpasswd to test fixture * Set env vars in test * Request the file that actually exists, maybe? * Is context the key? * Perhaps app.run needs to come to the party too? * Move `auth_basic` from `location` to `server` * Set `basic_auth` as true if env `basic_auth_username` exists * Fix htpasswd generation when basic_auth is false * Append env password instead truncating * Fix typo * Fix config example in README --- README.md | 14 ++++++++ scripts/boot | 3 ++ scripts/config/lib/nginx_config.rb | 6 ++++ scripts/config/make-htpasswd | 16 +++++++++ scripts/config/templates/nginx.conf.erb | 5 +++ spec/fixtures/basic_auth/.htpasswd | 1 + spec/fixtures/basic_auth/public_html/foo.html | 1 + spec/fixtures/basic_auth/static.json | 3 ++ spec/simple_spec.rb | 34 +++++++++++++++++++ 9 files changed, 83 insertions(+) create mode 100755 scripts/config/make-htpasswd create mode 100644 spec/fixtures/basic_auth/.htpasswd create mode 100644 spec/fixtures/basic_auth/public_html/foo.html create mode 100644 spec/fixtures/basic_auth/static.json diff --git a/README.md b/README.md index 979297bc..44e11606 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,20 @@ You can redirect all HTTP requests to HTTPS. } ``` +#### Basic Authentication + +You can enable Basic Authentication so all requests require authentication. + +``` +{ + "basic_auth": true +} +``` + +This will generate `.htpasswd` using environment variables `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` if they are present. Otherwise it will use a standard `.htpasswd` file present in the `app` directory. + +Passwords set via `BASIC_AUTH_PASSWORD` can be generated using OpenSSL or Apache Utils. For instance: `openssl passwd -apr1`. + #### Proxy Backends For single page web applications like Ember, it's common to back the application with another app that's hosted on Heroku. The down side of separating out these two applications is that now you have to deal with CORS. To get around this (but at the cost of some latency) you can have the static buildpack proxy apps to your backend at a mountpoint. For instance, we can have all the api requests live at `/api/` which actually are just requests to our API server. diff --git a/scripts/boot b/scripts/boot index 350435ea..8a0db662 100755 --- a/scripts/boot +++ b/scripts/boot @@ -17,6 +17,9 @@ esac "${HERE}/config/make-config" +# Create .htpasswd if BASIC_AUTH_USERNAME and BASIC_AUTH_PASSWORD are provided +"${HERE}/config/make-htpasswd" + # make a shared pipe; we'll write the name of the process that exits to it once # that happens, and wait for that event below this particular call works on # Linux and Mac OS (will create a literal ".XXXXXX" on Mac, but that doesn't diff --git a/scripts/config/lib/nginx_config.rb b/scripts/config/lib/nginx_config.rb index 77c2c473..d4508471 100644 --- a/scripts/config/lib/nginx_config.rb +++ b/scripts/config/lib/nginx_config.rb @@ -8,6 +8,8 @@ class NginxConfig encoding: "UTF-8", clean_urls: false, https_only: false, + basic_auth: false, + basic_auth_htpasswd_path: "/app/.htpasswd", worker_connections: 512, resolver: "8.8.8.8", logging: { @@ -45,6 +47,10 @@ def initialize(json_file) json["clean_urls"] ||= DEFAULT[:clean_urls] json["https_only"] ||= DEFAULT[:https_only] + json["basic_auth"] = true unless ENV['BASIC_AUTH_USERNAME'].nil? + json["basic_auth"] ||= DEFAULT[:basic_auth] + json["basic_auth_htpasswd_path"] ||= DEFAULT[:basic_auth_htpasswd_path] + json["routes"] ||= {} json["routes"] = NginxConfigUtil.parse_routes(json["routes"]) diff --git a/scripts/config/make-htpasswd b/scripts/config/make-htpasswd new file mode 100755 index 00000000..b1443a11 --- /dev/null +++ b/scripts/config/make-htpasswd @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby + +require 'json' + +USER_CONFIG = "/app/static.json" + +config = {} +config = JSON.parse(File.read(USER_CONFIG)) if File.exist?(USER_CONFIG) + +HTPASSWD = config["basic_auth_htpasswd_path"] || '/app/.htpasswd' +USERNAME = ENV["BASIC_AUTH_USERNAME"] +PASSWORD = ENV["BASIC_AUTH_PASSWORD"] + +htpasswd = "#{USERNAME}:#{PASSWORD}" unless (USERNAME.nil? || PASSWORD.nil?) + +File.open(HTPASSWD, 'a') { |file| file.puts(htpasswd) } if !htpasswd.nil? diff --git a/scripts/config/templates/nginx.conf.erb b/scripts/config/templates/nginx.conf.erb index 8d0045d4..930a335c 100644 --- a/scripts/config/templates/nginx.conf.erb +++ b/scripts/config/templates/nginx.conf.erb @@ -50,6 +50,11 @@ http { resolver <%= resolver %>; <% end %> + <% if basic_auth %> + auth_basic "Restricted"; + auth_basic_user_file <%= basic_auth_htpasswd_path %>; + <% end %> + location / { mruby_post_read_handler /app/bin/config/lib/ngx_mruby/headers.rb cache; mruby_set $fallback /app/bin/config/lib/ngx_mruby/routes_fallback.rb cache; diff --git a/spec/fixtures/basic_auth/.htpasswd b/spec/fixtures/basic_auth/.htpasswd new file mode 100644 index 00000000..1bad6472 --- /dev/null +++ b/spec/fixtures/basic_auth/.htpasswd @@ -0,0 +1 @@ +test:$apr1$Dnavu2z9$ZFxQn/mXVQoeYGD.tA2bW/ diff --git a/spec/fixtures/basic_auth/public_html/foo.html b/spec/fixtures/basic_auth/public_html/foo.html new file mode 100644 index 00000000..323fae03 --- /dev/null +++ b/spec/fixtures/basic_auth/public_html/foo.html @@ -0,0 +1 @@ +foobar diff --git a/spec/fixtures/basic_auth/static.json b/spec/fixtures/basic_auth/static.json new file mode 100644 index 00000000..be267815 --- /dev/null +++ b/spec/fixtures/basic_auth/static.json @@ -0,0 +1,3 @@ +{ + "basic_auth": true +} diff --git a/spec/simple_spec.rb b/spec/simple_spec.rb index c47db7b0..5f453567 100644 --- a/spec/simple_spec.rb +++ b/spec/simple_spec.rb @@ -182,6 +182,40 @@ end end + describe "basic_auth" do + context "static.json without basic_auth key" do + let(:name) { "hello_world" } + + let(:env) { + { + "BASIC_AUTH_USERNAME" => "test", + "BASIC_AUTH_PASSWORD" => "$apr1$Dnavu2z9$ZFxQn/mXVQoeYGD.tA2bW/" + } + } + + it "should require authentication" do + response = app.get("/index.html") + expect(response.code).to eq("401") + end + end + + context "static.json with basic_auth key and .htpasswd" do + let(:name) { "basic_auth" } + + let(:env) { + { + "BASIC_AUTH_USERNAME" => "test", + "BASIC_AUTH_PASSWORD" => "$apr1$/pb2/xQR$cn7UPcTOLymIH1ZMe.NfO." + } + } + + it "should require authentication" do + response = app.get("/foo.html") + expect(response.code).to eq("401") + end + end + end + describe "custom error pages" do let(:name) { "custom_error_pages" }