├── .gitignore ├── .rspec ├── spec ├── spec_helper.rb └── integration │ └── integration_spec.rb ├── app.rb ├── Gemfile ├── README.md ├── config ├── script ├── compile └── bootstrap ├── Rakefile ├── nginx.conf ├── Gemfile.lock └── ngx_http_auth_token_module.c /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format documentation -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'redis' 3 | require 'curb' 4 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | 3 | get '/app' do 4 | "Hello #{request.env['HTTP_X_USER_ID']}" 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "rake" 4 | gem "rspec" 5 | gem "redis" 6 | gem "curb" 7 | gem "sinatra" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NGINX Auth Token Module 2 | 3 | This repository holds the complete code example from my [NGINX module tutorial for AirPair](http://www.airpair.com/nginx/extending-nginx-tutorial) 4 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | CORE_LIBS="$CORE_LIBS -lhiredis" 2 | ngx_addon_name=ngx_http_auth_token_module 3 | HTTP_MODULES="$HTTP_MODULES ngx_http_auth_token_module" 4 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_auth_token_module.c" -------------------------------------------------------------------------------- /script/compile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd "vendor" 4 | pushd "nginx-1.18.0" 5 | CFLAGS="-g -O0" ./configure \ 6 | --with-debug \ 7 | --prefix=$(pwd)/../../build/nginx \ 8 | --conf-path=conf/nginx.conf \ 9 | --error-log-path=logs/error.log \ 10 | --http-log-path=logs/access.log \ 11 | --add-module=../../ 12 | make 13 | make install 14 | popd 15 | popd 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:integration) do |t| 5 | t.pattern = "spec/**/*_spec.rb" 6 | end 7 | 8 | namespace :nginx do 9 | desc "Starts NGINX" 10 | task :start do 11 | `build/nginx/sbin/nginx` 12 | sleep 1 13 | end 14 | 15 | desc "Stops NGINX" 16 | task :stop do 17 | `build/nginx/sbin/nginx -s stop` 18 | end 19 | 20 | desc "Recompiles NGINX" 21 | task :compile do 22 | sh "script/compile" 23 | end 24 | end 25 | 26 | desc "Bootstraps the local development environment" 27 | task :bootstrap do 28 | unless Dir.exists?("build") and Dir.exists?("vendor") 29 | sh "script/bootstrap" 30 | end 31 | end 32 | 33 | desc "Run the integration tests" 34 | task :default => [:bootstrap, "nginx:start", :integration, "nginx:stop"] 35 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | auth_token_enabled off; 7 | auth_token_redis_host "localhost"; 8 | auth_token_redis_port 6379; 9 | auth_token_cookie_name "auth_token"; 10 | auth_token_redirect_location "http://google.com"; 11 | 12 | upstream app { 13 | server localhost:4567; 14 | } 15 | 16 | server { 17 | listen 8888; 18 | 19 | location / { 20 | auth_token_enabled on; 21 | } 22 | 23 | location /loc { 24 | auth_token_enabled on; 25 | auth_token_redirect_location "http://google.com/location"; 26 | } 27 | 28 | location /app { 29 | proxy_pass http://app; 30 | } 31 | } 32 | 33 | server { 34 | listen 8889; 35 | auth_token_redirect_location "http://google.com/server"; 36 | auth_token_header_name "X-Authorization"; 37 | 38 | location / { 39 | auth_token_enabled on; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | curb (0.9.10) 5 | diff-lcs (1.3) 6 | mustermann (1.1.1) 7 | ruby2_keywords (~> 0.0.1) 8 | rack (2.2.3) 9 | rack-protection (2.0.8.1) 10 | rack 11 | rake (13.0.1) 12 | redis (4.2.1) 13 | rspec (3.9.0) 14 | rspec-core (~> 3.9.0) 15 | rspec-expectations (~> 3.9.0) 16 | rspec-mocks (~> 3.9.0) 17 | rspec-core (3.9.2) 18 | rspec-support (~> 3.9.3) 19 | rspec-expectations (3.9.2) 20 | diff-lcs (>= 1.2.0, < 2.0) 21 | rspec-support (~> 3.9.0) 22 | rspec-mocks (3.9.1) 23 | diff-lcs (>= 1.2.0, < 2.0) 24 | rspec-support (~> 3.9.0) 25 | rspec-support (3.9.3) 26 | ruby2_keywords (0.0.2) 27 | sinatra (2.0.8.1) 28 | mustermann (~> 1.0) 29 | rack (~> 2.0) 30 | rack-protection (= 2.0.8.1) 31 | tilt (~> 2.0) 32 | tilt (2.0.10) 33 | 34 | PLATFORMS 35 | ruby 36 | 37 | DEPENDENCIES 38 | curb 39 | rake 40 | redis 41 | rspec 42 | sinatra 43 | 44 | BUNDLED WITH 45 | 2.1.4 46 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o nounset 4 | set -o errexit 5 | 6 | DIR=$(pwd) 7 | BUILDDIR=$DIR/build 8 | NGINX_DIR=nginx 9 | NGINX_VERSION=1.18.0 10 | 11 | clean () { 12 | rm -rf build vendor 13 | } 14 | 15 | setup_local_directories () { 16 | if [ ! -d $BUILDDIR ]; then 17 | mkdir $BUILDDIR > /dev/null 2>&1 18 | mkdir $BUILDDIR/$NGINX_DIR > /dev/null 2>&1 19 | fi 20 | 21 | if [ ! -d "vendor" ]; then 22 | mkdir vendor > /dev/null 2>&1 23 | fi 24 | } 25 | 26 | install_nginx () { 27 | if [ ! -d "vendor/nginx-$NGINX_VERSION" ]; then 28 | pushd vendor > /dev/null 2>&1 29 | curl -s -L -O "http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz" 30 | tar xzf "nginx-$NGINX_VERSION.tar.gz" 31 | pushd "nginx-$NGINX_VERSION" > /dev/null 2>&1 32 | ./configure \ 33 | --with-debug \ 34 | --prefix=$(pwd)/../../build/nginx \ 35 | --conf-path=conf/nginx.conf \ 36 | --error-log-path=logs/error.log \ 37 | --http-log-path=logs/access.log 38 | make 39 | make install 40 | popd > /dev/null 2>&1 41 | popd > /dev/null 2>&1 42 | ln -sf $(pwd)/nginx.conf $(pwd)/build/nginx/conf/nginx.conf 43 | else 44 | printf "NGINX already installed\n" 45 | fi 46 | } 47 | 48 | if [[ "$#" -eq 1 ]]; then 49 | if [[ "$1" == "clean" ]]; then 50 | clean 51 | else 52 | echo "clean is the only option" 53 | fi 54 | else 55 | setup_local_directories 56 | install_nginx 57 | fi 58 | -------------------------------------------------------------------------------- /spec/integration/integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Integration Specs" do 4 | 5 | before do 6 | @redis = Redis.new 7 | @redis.flushdb 8 | end 9 | 10 | after { @redis.flushdb } 11 | 12 | describe "Use cases" do 13 | it "Redirects when no auth token is present" do 14 | http = Curl.get("http://127.0.0.1:8888") do |http| 15 | http.headers['Cookie'] = "" 16 | end 17 | 18 | expect(http.response_code).to eq(302) 19 | expect(http.redirect_url).to eq("http://google.com/") 20 | end 21 | 22 | it "Redirects when the auth token is invalid" do 23 | @redis.set("test", "sucess") 24 | 25 | http = Curl.get("http://127.0.0.1:8888") do |http| 26 | http.headers['Cookie'] = "auth_token=invalid" 27 | end 28 | 29 | expect(http.response_code).to eq(302) 30 | end 31 | 32 | it "Allows the request when the auth token is valid" do 33 | @redis.set("test", "sucess") 34 | 35 | http = Curl.get("http://127.0.0.1:8888") do |http| 36 | http.headers['Cookie'] = "auth_token=test" 37 | end 38 | 39 | expect(http.response_code).to eq(200) 40 | end 41 | 42 | it "handles user ids longer than 7 characters" do 43 | @redis.set("f0f70003-f368-4266-a448-c45a96b8fc13", "user-longer-than-seven-characters") 44 | 45 | http = Curl.get("http://127.0.0.1:8888") do |http| 46 | http.headers['Cookie'] = "auth_token=f0f70003-f368-4266-a448-c45a96b8fc13" 47 | end 48 | 49 | expect(http.response_code).to eq(200) 50 | end 51 | 52 | it "respects the location section redirect directive" do 53 | http = Curl.get("http://127.0.0.1:8888/location") 54 | expect(http.response_code).to eq(302) 55 | expect(http.redirect_url).to eq("http://google.com/location") 56 | end 57 | 58 | it "respects the server section redirect directive" do 59 | http = Curl.get("http://127.0.0.1:8889/") 60 | expect(http.response_code).to eq(302) 61 | expect(http.redirect_url).to eq("http://google.com/server") 62 | end 63 | 64 | it "properly locates the authentication token in a header" do 65 | @redis.set("test", "sucess") 66 | 67 | http = Curl.get("http://127.0.0.1:8889") do |http| 68 | http.headers['X-Authorization'] = "test" 69 | end 70 | 71 | expect(http.response_code).to eq(200) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /ngx_http_auth_token_module.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include "hiredis/hiredis.h" 8 | 9 | 10 | typedef struct { 11 | ngx_str_t redis_host; 12 | ngx_int_t redis_port; 13 | } auth_token_main_conf_t; 14 | 15 | 16 | typedef struct { 17 | ngx_flag_t enabled; 18 | ngx_str_t redirect_location; 19 | ngx_str_t cookie_name; 20 | ngx_str_t header_name; 21 | } auth_token_loc_conf_t; 22 | 23 | 24 | ngx_module_t ngx_http_auth_token_module; 25 | 26 | 27 | static ngx_int_t 28 | lookup_user(auth_token_main_conf_t *conf, ngx_str_t *auth_token, ngx_str_t *user_id) 29 | { 30 | redisContext *context = redisConnect((const char*)conf->redis_host.data, conf->redis_port); 31 | redisReply *reply = redisCommand(context, "GET %s", auth_token->data); 32 | if (reply->type == REDIS_REPLY_NIL) { 33 | return NGX_DECLINED; 34 | } else { 35 | user_id->len = strlen(reply->str); 36 | user_id->data = (u_char *) reply->str; 37 | return NGX_OK; 38 | } 39 | } 40 | 41 | 42 | static ngx_int_t 43 | redirect(ngx_http_request_t *r, ngx_str_t *location) 44 | { 45 | ngx_table_elt_t *h; 46 | h = ngx_list_push(&r->headers_out.headers); 47 | h->hash = 1; 48 | ngx_str_set(&h->key, "Location"); 49 | h->value = *location; 50 | 51 | return NGX_HTTP_MOVED_TEMPORARILY; 52 | } 53 | 54 | 55 | static void 56 | append_user_id(ngx_http_request_t *r, ngx_str_t *user_id) 57 | { 58 | ngx_table_elt_t *h; 59 | h = ngx_list_push(&r->headers_in.headers); 60 | h->hash = 1; 61 | ngx_str_set(&h->key, "X-User-Id"); 62 | h->value = *user_id; 63 | } 64 | 65 | 66 | static ngx_int_t 67 | search_headers(ngx_http_request_t *r, auth_token_loc_conf_t *location_conf, ngx_str_t *token) 68 | { 69 | ngx_list_part_t *part; 70 | ngx_table_elt_t *h; 71 | ngx_uint_t i; 72 | 73 | part = &r->headers_in.headers.part; 74 | h = part->elts; 75 | 76 | for (i = 0; /**/; i++) { 77 | if (i >= part->nelts) { 78 | if (part->next == NULL) { 79 | break; 80 | } 81 | 82 | part = part->next; 83 | h = part->elts; 84 | i = 0; 85 | } 86 | 87 | if (ngx_strncmp(h[i].key.data, location_conf->header_name.data, h[i].key.len) == 0) { 88 | token->data = h[i].value.data; 89 | token->len = h[i].value.len; 90 | return NGX_OK; 91 | } 92 | } 93 | 94 | return NGX_DECLINED; 95 | } 96 | 97 | 98 | static ngx_int_t 99 | header_lookup(ngx_http_request_t *r, auth_token_loc_conf_t *location_conf, ngx_str_t *token) 100 | { 101 | ngx_int_t result = search_headers(r, location_conf, token); 102 | if (result == NGX_DECLINED) { 103 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Could not locate header %V", &location_conf->header_name); 104 | return NGX_DECLINED; 105 | } else { 106 | return NGX_OK; 107 | } 108 | } 109 | 110 | 111 | static ngx_int_t 112 | cookie_lookup(ngx_http_request_t *r, auth_token_loc_conf_t *location_conf, ngx_str_t *token) 113 | { 114 | ngx_int_t cookie_location; 115 | cookie_location = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &location_conf->cookie_name, token); 116 | if (cookie_location == NGX_DECLINED) { 117 | return NGX_DECLINED; 118 | } else { 119 | return NGX_OK; 120 | } 121 | } 122 | 123 | 124 | static ngx_int_t 125 | ngx_http_auth_token_handler(ngx_http_request_t *r) 126 | { 127 | if (r->main->internal) { 128 | return NGX_DECLINED; 129 | } 130 | 131 | auth_token_loc_conf_t *location_conf = ngx_http_get_module_loc_conf(r, ngx_http_auth_token_module); 132 | 133 | if (!location_conf->enabled || location_conf->enabled == NGX_CONF_UNSET) { 134 | return NGX_DECLINED; 135 | } 136 | 137 | auth_token_main_conf_t *main_conf = ngx_http_get_module_main_conf(r, ngx_http_auth_token_module); 138 | 139 | ngx_str_t auth_token; 140 | ngx_int_t search_result; 141 | if (location_conf->header_name.len != 0) { 142 | search_result = header_lookup(r, location_conf, &auth_token); 143 | } else { 144 | search_result = cookie_lookup(r, location_conf, &auth_token); 145 | } 146 | 147 | if (search_result == NGX_DECLINED) { 148 | return redirect(r, &location_conf->redirect_location); 149 | } 150 | 151 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Looking up user by auth token %V", &auth_token); 152 | 153 | ngx_str_t user_id; 154 | ngx_int_t lookup_result = lookup_user(main_conf, &auth_token, &user_id); 155 | 156 | if (lookup_result == NGX_DECLINED) { 157 | return redirect(r, &location_conf->redirect_location); 158 | } else { 159 | append_user_id(r, &user_id); 160 | return NGX_DECLINED; 161 | } 162 | } 163 | 164 | 165 | static ngx_int_t 166 | ngx_http_auth_token_init(ngx_conf_t *cf) 167 | { 168 | ngx_http_handler_pt *h; 169 | ngx_http_core_main_conf_t *cmcf; 170 | 171 | cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); 172 | 173 | h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); 174 | if (h == NULL) { 175 | return NGX_ERROR; 176 | } 177 | 178 | *h = ngx_http_auth_token_handler; 179 | 180 | return NGX_OK; 181 | } 182 | 183 | 184 | static void* 185 | ngx_http_auth_token_create_main_conf(ngx_conf_t *cf) 186 | { 187 | auth_token_main_conf_t *conf; 188 | 189 | conf = ngx_pcalloc(cf->pool, sizeof(auth_token_main_conf_t)); 190 | if (conf == NULL) { 191 | return NULL; 192 | } 193 | 194 | conf->redis_port = NGX_CONF_UNSET_UINT; 195 | 196 | return conf; 197 | } 198 | 199 | static void* 200 | ngx_http_auth_token_create_loc_conf(ngx_conf_t *cf) 201 | { 202 | auth_token_loc_conf_t *conf; 203 | 204 | conf = ngx_pcalloc(cf->pool, sizeof(auth_token_loc_conf_t)); 205 | if (conf == NULL) { 206 | return NULL; 207 | } 208 | 209 | conf->enabled = NGX_CONF_UNSET; 210 | 211 | return conf; 212 | } 213 | 214 | static char* 215 | ngx_http_auth_token_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) 216 | { 217 | auth_token_loc_conf_t *prev = (auth_token_loc_conf_t*)parent; 218 | auth_token_loc_conf_t *conf = (auth_token_loc_conf_t*)child; 219 | 220 | ngx_conf_merge_value(conf->enabled, prev->enabled, 0); 221 | ngx_conf_merge_str_value(conf->redirect_location, prev->redirect_location, ""); 222 | ngx_conf_merge_str_value(conf->cookie_name, prev->cookie_name, ""); 223 | ngx_conf_merge_str_value(conf->header_name, prev->header_name, ""); 224 | 225 | return NGX_CONF_OK; 226 | } 227 | 228 | static ngx_command_t ngx_http_auth_token_commands[] = { 229 | { 230 | ngx_string("auth_token_redis_host"), 231 | NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1, 232 | ngx_conf_set_str_slot, 233 | NGX_HTTP_MAIN_CONF_OFFSET, 234 | offsetof(auth_token_main_conf_t, redis_host), 235 | NULL 236 | }, 237 | { 238 | ngx_string("auth_token_redis_port"), 239 | NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1, 240 | ngx_conf_set_num_slot, 241 | NGX_HTTP_MAIN_CONF_OFFSET, 242 | offsetof(auth_token_main_conf_t, redis_port), 243 | NULL 244 | }, 245 | { 246 | ngx_string("auth_token_cookie_name"), 247 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 248 | ngx_conf_set_str_slot, 249 | NGX_HTTP_LOC_CONF_OFFSET, 250 | offsetof(auth_token_loc_conf_t, cookie_name), 251 | NULL 252 | }, 253 | { 254 | ngx_string("auth_token_header_name"), 255 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 256 | ngx_conf_set_str_slot, 257 | NGX_HTTP_LOC_CONF_OFFSET, 258 | offsetof(auth_token_loc_conf_t, header_name), 259 | NULL 260 | }, 261 | { 262 | ngx_string("auth_token_redirect_location"), 263 | NGX_HTTP_MAIN_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, 264 | ngx_conf_set_str_slot, 265 | NGX_HTTP_LOC_CONF_OFFSET, 266 | offsetof(auth_token_loc_conf_t, redirect_location), 267 | NULL 268 | }, 269 | { 270 | ngx_string("auth_token_enabled"), 271 | NGX_HTTP_MAIN_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 272 | ngx_conf_set_flag_slot, 273 | NGX_HTTP_LOC_CONF_OFFSET, 274 | offsetof(auth_token_loc_conf_t, enabled), 275 | NULL 276 | }, 277 | 278 | ngx_null_command 279 | }; 280 | 281 | 282 | static ngx_http_module_t ngx_http_auth_token_module_ctx = { 283 | NULL, /* preconfiguration */ 284 | ngx_http_auth_token_init, /* postconfiguration */ 285 | ngx_http_auth_token_create_main_conf, /* create main configuration */ 286 | NULL, /* init main configuration */ 287 | NULL, /* create server configuration */ 288 | NULL, /* merge server configuration */ 289 | ngx_http_auth_token_create_loc_conf, /* create location configuration */ 290 | ngx_http_auth_token_merge_loc_conf /* merge location configuration */ 291 | }; 292 | 293 | 294 | ngx_module_t ngx_http_auth_token_module = { 295 | NGX_MODULE_V1, 296 | &ngx_http_auth_token_module_ctx, /* module context */ 297 | ngx_http_auth_token_commands, /* module directives */ 298 | NGX_HTTP_MODULE, /* module type */ 299 | NULL, /* init master */ 300 | NULL, /* init module */ 301 | NULL, /* init process */ 302 | NULL, /* init thread */ 303 | NULL, /* exit thread */ 304 | NULL, /* exit process */ 305 | NULL, /* exit master */ 306 | NGX_MODULE_V1_PADDING 307 | }; 308 | --------------------------------------------------------------------------------