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" }