├── .env.example ├── .gitignore ├── Dockerfile ├── Dockerfile-dev ├── LICENSE ├── README.md ├── app ├── app.lua ├── config.lua ├── entrypoint.sh ├── github.lua ├── google.lua ├── mime.types ├── models.lua ├── nginx.conf └── utils.lua ├── docker-compose-dev.yml └── docker-compose.yml /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_CLIENT_ID= 2 | GITHUB_CLIENT_SECRET= 3 | GITHUB_REDIRECT_URL= 4 | GOOGLE_CLIENT_ID= 5 | GOOGLE_CLIENT_SECRET= 6 | GOOGLE_REDIRECT_URL= 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | diff 4 | *.err 5 | *.orig 6 | *.log 7 | *.rej 8 | *.swo 9 | *.swp 10 | *.vi 11 | *~ 12 | *.sass-cache 13 | 14 | # OS or Editor folders 15 | .DS_Store 16 | .cache 17 | .project 18 | .settings 19 | .tmproj 20 | nbproject 21 | Thumbs.db 22 | 23 | # Lua specific files 24 | app/*_temp/ 25 | app/*.compiled 26 | app/logs/ 27 | 28 | # env file 29 | .env 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openresty/openresty:alpine-fat 2 | 3 | MAINTAINER Sebastian Ruml 4 | 5 | RUN apk add --update \ 6 | openssl-dev bash \ 7 | && rm /var/cache/apk/* 8 | 9 | RUN /usr/local/openresty/luajit/bin/luarocks install lapis 10 | RUN /usr/local/openresty/luajit/bin/luarocks install penlight 11 | 12 | RUN mkdir /app 13 | 14 | WORKDIR /app 15 | 16 | ADD ./app /app 17 | 18 | ENV LAPIS_OPENRESTY "/usr/local/openresty/bin/openresty" 19 | 20 | EXPOSE 8080 21 | 22 | ENTRYPOINT ["/app/entrypoint.sh"] 23 | 24 | CMD ["server", "production"] 25 | -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM openresty/openresty:alpine-fat 2 | 3 | MAINTAINER Sebastian Ruml 4 | 5 | RUN apk add --update \ 6 | openssl-dev bash \ 7 | && rm /var/cache/apk/* 8 | 9 | RUN /usr/local/openresty/luajit/bin/luarocks install lapis 10 | RUN /usr/local/openresty/luajit/bin/luarocks install penlight 11 | 12 | RUN mkdir /app 13 | WORKDIR /app 14 | 15 | ENV LAPIS_OPENRESTY "/usr/local/openresty/bin/openresty" 16 | 17 | VOLUME /app 18 | 19 | EXPOSE 8080 20 | 21 | ENTRYPOINT ["/app/entrypoint.sh"] 22 | 23 | CMD ["server", "development"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Sebastian Ruml 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micro-auth 2 | 3 | A microservice that makes adding authentication with Google or GitHub to your application easy. 4 | 5 | This service allows you to use Google and GitHub OAuth2 service to add authentication to your applications in a very straightforward way. 6 | 7 | It's build with [Nginx/OpenResty](https://openresty.org/en/), [Lapis](http://leafo.net/lapis/) and [Docker](https://www.docker.com/). This enables the service to be very performant and requires only minimal system resources. 8 | 9 | 10 | ## Features 11 | 12 | * Use [Google](https://developers.google.com/identity/protocols/OAuth2) or [GitHub](https://developer.github.com/v3/oauth/) OAuth2 authentication to provide a simple login mechanism for your application. 13 | * Quick setup with [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/). With support for development and production environments. 14 | * Based on [Lapis](http://leafo.net/lapis) and [Nginx/OpenResty](https://openresty.org/en/). 15 | 16 | 17 | ## Getting started 18 | 19 | * Install (if you don't have them): 20 | * [Docker](https://www.docker.com/) 21 | * [docker-compose](https://docs.docker.com/compose/) 22 | * Setup required [environment variables](#environment-variables) 23 | * Run in _development_ mode: 24 | * Run the application with `docker-compose -f docker-compose-dev.yml up` 25 | * Run in _production_ mode: 26 | * Run the application with `docker-compose up` 27 | * Once `micro-auth` is running, you can point your login to one of the following urls (URLs are for development mode): 28 | * `http://localhost:8080/auth/github`: For GitHub login 29 | * `http://localhost:8080/auth/google`: For Google login 30 | 31 | 32 | ## Authentication Services 33 | 34 | ### Google 35 | 36 | **Setup** 37 | 38 | Visit [Google Developers Console](https://console.developers.google.com) and create a new application on Google. 39 | Then go to [Credentials](https://console.developers.google.com/apis/credentials) and create a new _OAuth Client ID_. 40 | Now, get the _Client ID_ and _Client secret_. 41 | 42 | **Endpoints** 43 | 44 | * `http://localhost:8080/auth/google`: Endpoint for Google authentication. Point your application to this endpoint to login with Google. 45 | 46 | **Results** 47 | 48 | After successful authentication with Google the user is redirect to the URL specified in `GOOGLE_REDIRECT_URL` with the access token saved in the `access_token` query parameter. 49 | 50 | ### GitHub 51 | 52 | **Setup** 53 | 54 | Visit [GitHub](https://github.com/settings/applications/new) and create a new application on GitHub to get your client id and secret. 55 | 56 | **Endpoints** 57 | 58 | * `http://localhost:8080/auth/github`: Endpoint for GitHub authentication. Point your application to this endpoint to login with Github. 59 | 60 | **Results** 61 | 62 | After successful authentication with GitHub the user is redirect to the URL specified in `GITHUB_REDIRECT_URL` with the access token saved in the `access_token` query parameter. 63 | 64 | 65 | ## Environment variables 66 | 67 | To use the service you must set some required environment variables. These variables can be set in the `.env` file. Just copy `.env.example` to `.env` 68 | 69 | ``` 70 | $ cp .env.example .env 71 | ``` 72 | 73 | end set the required variables. 74 | 75 | ## Secrets in docker swarm 76 | Setting the above environment variables with `_FILE` pointed at the secret mount inside the container. `-e GOOGLE_SECRET_FILE=/run/secrets/google_secret`. This will set the contents on the file as the value of the environment variable. 77 | 78 | ### General 79 | 80 | * `APP_URL`: Specify the URL of `micro-auth` (default: `http://localhost:8080` in development mode). The `APP_URL` must be set in production mode. 81 | 82 | ### Google 83 | 84 | * `GOOGLE_CLIENT_ID`: The Google application client id (required) 85 | * `GOOGLE_CLIENT_SECRET`: The Google application client secret (required) 86 | * `GOOGLE_REDIRECT_URL`: The url to redirect the user once the authentication was successful 87 | 88 | ### GitHub 89 | 90 | * `GITHUB_CLIENT_ID`: The GitHub application client id (required) 91 | * `GITHUB_CLIENT_SECRET`: The GitHub application client secret (required) 92 | * `GITHUB_REDIRECT_URL`: The url to redirect the user once the authentication was successful 93 | 94 | 95 | ## License 96 | 97 | See [LICENSE](./LICENSE) 98 | 99 | 100 | ## Credits 101 | 102 | * [micro-github](https://github.com/mxstbr/micro-github) 103 | -------------------------------------------------------------------------------- /app/app.lua: -------------------------------------------------------------------------------- 1 | local lapis = require("lapis") 2 | 3 | local github = require("github") 4 | local google = require("google") 5 | 6 | local app = lapis.Application() 7 | 8 | 9 | -- Github authorization 10 | app:get("/auth/github", github.authorize) 11 | app:get("/auth/github/callback", github.callback) 12 | 13 | -- Google authorization 14 | app:get("/auth/google", google.authorize) 15 | app:get("/auth/google/callback", google.callback) 16 | 17 | 18 | return app 19 | -------------------------------------------------------------------------------- /app/config.lua: -------------------------------------------------------------------------------- 1 | -- config.lua 2 | local config = require("lapis.config") 3 | 4 | config({"development", "production"}, { 5 | app_url = os.getenv("APP_URL") or "http://localhost:8080", 6 | github = { 7 | client_id = os.getenv("GITHUB_CLIENT_ID"), 8 | client_secret = os.getenv("GITHUB_CLIENT_SECRET"), 9 | redirect_uri = os.getenv("GITHUB_REDIRECT_URL") 10 | }, 11 | google = { 12 | client_id = os.getenv("GOOGLE_CLIENT_ID"), 13 | client_secret = os.getenv("GOOGLE_CLIENT_SECRET"), 14 | redirect_uri = os.getenv("GOOGLE_REDIRECT_URL"), 15 | scope = os.getenv("GOOGLE_SCOPE") or "https://www.googleapis.com/auth/plus.me" 16 | } 17 | }) 18 | 19 | config("development", { 20 | port = 8080, 21 | code_cache = "off" 22 | }) 23 | 24 | config("production", { 25 | port = 8080, 26 | code_cache = "on" 27 | }) 28 | -------------------------------------------------------------------------------- /app/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # usage: file_env VAR [DEFAULT] 4 | # ie: file_env 'XYZ_DB_PASSWORD' 'example' 5 | # (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of 6 | # "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) 7 | file_env() { 8 | local var="$1" 9 | local fileVar="${var}_FILE" 10 | local def="${2:-}" 11 | if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then 12 | echo >&2 "error: both $var and $fileVar are set (but are exclusive)" 13 | exit 1 14 | fi 15 | local val="$def" 16 | if [ "${!var:-}" ]; then 17 | val="${!var}" 18 | elif [ "${!fileVar:-}" ]; then 19 | val="$(< "${!fileVar}")" 20 | fi 21 | export "$var"="$val" 22 | unset "$fileVar" 23 | } 24 | 25 | file_env 'GITHUB_CLIENT_ID' 26 | file_env 'GITHUB_CLIENT_SECRET' 27 | file_env 'GITHUB_REDIRECT_URL' 28 | file_env 'GOOGLE_CLIENT_ID' 29 | file_env 'GOOGLE_CLIENT_SECRET' 30 | 31 | /usr/local/openresty/luajit/bin/lapis "$@" 32 | -------------------------------------------------------------------------------- /app/github.lua: -------------------------------------------------------------------------------- 1 | local config = require("lapis.config").get() 2 | local lapis_util = require("lapis.util") 3 | local http = require("lapis.nginx.http") 4 | local utils = require("utils") 5 | 6 | local GITHUB_LOGIN_BASE_URL = "https://github.com/login/oauth" 7 | 8 | local _M = {} 9 | 10 | 11 | function _M.authorize() 12 | local url = GITHUB_LOGIN_BASE_URL .. "/authorize" 13 | local qs = { 14 | client_id = config.github.client_id, 15 | redirect_uri = config.app_url .. "/auth/github/callback" 16 | } 17 | 18 | return utils.createRedirectHTML(url, qs) 19 | end 20 | 21 | function _M.callback(self) 22 | local code = self.params.code 23 | 24 | if code == nil or code == "" then 25 | return {utils.createRedirectHTML(config.github.redirect_uri, { error = "no code found"}), status = 401} 26 | end 27 | 28 | local githubUrl = GITHUB_LOGIN_BASE_URL .. "/access_token" 29 | 30 | local body, status_code, headers = http.simple({ 31 | url = githubUrl, 32 | method = "POST", 33 | headers = { 34 | ["Accept"] = "application/json", 35 | ["content-type"] = "application/x-www-form-urlencoded" 36 | }, 37 | body = { 38 | client_id = config.github.client_id, 39 | client_secret = config.github.client_secret, 40 | code = code 41 | } 42 | }) 43 | 44 | if status_code == 200 then 45 | local data = lapis_util.from_json(body) 46 | 47 | if data.error ~= nil then 48 | return {utils.createRedirectHTML(config.github.redirect_uri, { error = data.error_description }), status = 401} 49 | end 50 | 51 | return {utils.createRedirectHTML(config.github.redirect_uri, { access_token = data.access_token }), status = 200} 52 | elseif status_code == 500 then 53 | return {utils.createRedirectHTML(config.github.redirect_uri, { error = "Github server error" }), status = 500} 54 | else 55 | return {utils.createRedirectHTML(config.github.redirect_uri, { error = "Please provide required environment variable" }), status = 500} 56 | end 57 | end 58 | 59 | return _M 60 | -------------------------------------------------------------------------------- /app/google.lua: -------------------------------------------------------------------------------- 1 | local config = require("lapis.config").get() 2 | local lapis_util = require("lapis.util") 3 | local http = require("lapis.nginx.http") 4 | local utils = require("utils") 5 | 6 | 7 | local _M = {} 8 | 9 | function _M.authorize() 10 | local url = "https://accounts.google.com/o/oauth2/v2/auth" 11 | local scope = config.google.scope 12 | local qs = { 13 | client_id = config.google.client_id, 14 | redirect_uri = config.app_url .. "/auth/google/callback", 15 | response_type = "code", 16 | scope = scope 17 | } 18 | 19 | return utils.createRedirectHTML(url, qs) 20 | end 21 | 22 | function _M.callback(self) 23 | local googleAuthUrl = "https://www.googleapis.com/oauth2/v4/token" 24 | local code = self.params.code 25 | local error = self.params.error 26 | 27 | if code == nil or code == "" or error then 28 | return {utils.createRedirectHTML(config.google.redirect_uri, { error = "access denied"}), status = 401} 29 | end 30 | 31 | local body, status_code, headers = http.simple({ 32 | url = googleAuthUrl, 33 | method = "POST", 34 | headers = { 35 | ["Accept"] = "application/json", 36 | ["content-type"] = "application/x-www-form-urlencoded" 37 | }, 38 | body = { 39 | client_id = config.google.client_id, 40 | client_secret = config.google.client_secret, 41 | code = code, 42 | grant_type = "authorization_code", 43 | redirect_uri = config.app_url .. "/auth/google/callback" 44 | } 45 | }) 46 | 47 | if status_code == 200 then 48 | local data = lapis_util.from_json(body) 49 | 50 | if data.error ~= nil then 51 | return {utils.createRedirectHTML(config.google.redirect_uri, { error = data.error_description }), status = 401} 52 | end 53 | 54 | local resp = { 55 | access_token = data.access_token, 56 | refresh_token = data.refresh_token, 57 | expires_in = data.expires_in 58 | } 59 | return {utils.createRedirectHTML(config.google.redirect_uri, resp), status = 200} 60 | elseif status_code == 500 then 61 | return {utils.createRedirectHTML(config.google.redirect_uri, { error = "Google server error" }), status = 500} 62 | else 63 | return {utils.createRedirectHTML(config.google.redirect_uri, { error = "Please provide required environment variable" }), status = 500} 64 | end 65 | end 66 | 67 | return _M 68 | -------------------------------------------------------------------------------- /app/mime.types: -------------------------------------------------------------------------------- 1 | types { 2 | text/html html htm shtml; 3 | text/css css; 4 | text/xml xml; 5 | image/gif gif; 6 | image/jpeg jpeg jpg; 7 | application/x-javascript js; 8 | application/atom+xml atom; 9 | application/rss+xml rss; 10 | 11 | text/mathml mml; 12 | text/plain txt; 13 | text/vnd.sun.j2me.app-descriptor jad; 14 | text/vnd.wap.wml wml; 15 | text/x-component htc; 16 | 17 | image/png png; 18 | image/tiff tif tiff; 19 | image/vnd.wap.wbmp wbmp; 20 | image/x-icon ico; 21 | image/x-jng jng; 22 | image/x-ms-bmp bmp; 23 | image/svg+xml svg svgz; 24 | image/webp webp; 25 | 26 | application/java-archive jar war ear; 27 | application/mac-binhex40 hqx; 28 | application/msword doc; 29 | application/pdf pdf; 30 | application/postscript ps eps ai; 31 | application/rtf rtf; 32 | application/vnd.ms-excel xls; 33 | application/vnd.ms-powerpoint ppt; 34 | application/vnd.wap.wmlc wmlc; 35 | application/vnd.google-earth.kml+xml kml; 36 | application/vnd.google-earth.kmz kmz; 37 | application/x-7z-compressed 7z; 38 | application/x-cocoa cco; 39 | application/x-java-archive-diff jardiff; 40 | application/x-java-jnlp-file jnlp; 41 | application/x-makeself run; 42 | application/x-perl pl pm; 43 | application/x-pilot prc pdb; 44 | application/x-rar-compressed rar; 45 | application/x-redhat-package-manager rpm; 46 | application/x-sea sea; 47 | application/x-shockwave-flash swf; 48 | application/x-stuffit sit; 49 | application/x-tcl tcl tk; 50 | application/x-x509-ca-cert der pem crt; 51 | application/x-xpinstall xpi; 52 | application/xhtml+xml xhtml; 53 | application/zip zip; 54 | 55 | application/octet-stream bin exe dll; 56 | application/octet-stream deb; 57 | application/octet-stream dmg; 58 | application/octet-stream eot; 59 | application/octet-stream iso img; 60 | application/octet-stream msi msp msm; 61 | 62 | audio/midi mid midi kar; 63 | audio/mpeg mp3; 64 | audio/ogg ogg; 65 | audio/x-m4a m4a; 66 | audio/x-realaudio ra; 67 | 68 | video/3gpp 3gpp 3gp; 69 | video/mp4 mp4; 70 | video/mpeg mpeg mpg; 71 | video/quicktime mov; 72 | video/webm webm; 73 | video/x-flv flv; 74 | video/x-m4v m4v; 75 | video/x-mng mng; 76 | video/x-ms-asf asx asf; 77 | video/x-ms-wmv wmv; 78 | video/x-msvideo avi; 79 | } 80 | -------------------------------------------------------------------------------- /app/models.lua: -------------------------------------------------------------------------------- 1 | local autoload = require("lapis.util").autoload 2 | return autoload("models") 3 | -------------------------------------------------------------------------------- /app/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes ${{NUM_WORKERS}}; 2 | error_log stderr debug; 3 | daemon off; 4 | pid logs/nginx.pid; 5 | 6 | # define environment variables 7 | env GITHUB_CLIENT_ID; 8 | env GITHUB_CLIENT_SECRET; 9 | env GITHUB_REDIRECT_URL; 10 | env GOOGLE_CLIENT_ID; 11 | env GOOGLE_CLIENT_SECRET; 12 | env GOOGLE_REDIRECT_URL; 13 | env APP_URL; 14 | 15 | events { 16 | worker_connections 1024; 17 | } 18 | 19 | http { 20 | include mime.types; 21 | 22 | server { 23 | listen ${{PORT}}; 24 | lua_code_cache ${{CODE_CACHE}}; 25 | 26 | location / { 27 | default_type text/html; 28 | 29 | set $_url ""; 30 | 31 | content_by_lua ' 32 | require("lapis").serve("app") 33 | '; 34 | } 35 | 36 | location /static/ { 37 | alias static/; 38 | } 39 | 40 | location /favicon.ico { 41 | alias static/favicon.ico; 42 | } 43 | 44 | location /proxy { 45 | internal; 46 | rewrite_by_lua " 47 | local req = ngx.req 48 | 49 | for k,v in pairs(req.get_headers()) do 50 | if k ~= 'content-length' then 51 | req.clear_header(k) 52 | end 53 | end 54 | 55 | if ngx.ctx.headers then 56 | for k,v in pairs(ngx.ctx.headers) do 57 | req.set_header(k, v) 58 | end 59 | end 60 | "; 61 | 62 | resolver 8.8.8.8; 63 | proxy_http_version 1.1; 64 | proxy_pass $_url; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/utils.lua: -------------------------------------------------------------------------------- 1 | local Template = require ('pl.text').Template 2 | local util = require("lapis.util") 3 | 4 | 5 | local _M = {} 6 | 7 | function _M.createRedirectHTML(url, data) 8 | local htmlTemplate = [[ 9 | 10 | Redirecting… 11 | 12 | ]] 13 | local t = Template(htmlTemplate) 14 | 15 | return t:substitute {url = url .. "?" .. util.encode_query_string(data)} 16 | end 17 | 18 | return _M 19 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: ./Dockerfile-dev 8 | volumes: 9 | - ./app:/app 10 | ports: 11 | - 8080:8080 12 | environment: 13 | - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID} 14 | - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET} 15 | - GITHUB_REDIRECT_URL=${GITHUB_REDIRECT_URL} 16 | - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} 17 | - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} 18 | - GOOGLE_REDIRECT_URL=${GOOGLE_REDIRECT_URL} 19 | command: server development 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | app: 5 | build: . 6 | ports: 7 | - 8080:8080 8 | environment: 9 | - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID} 10 | - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET} 11 | - GITHUB_REDIRECT_URL=${GITHUB_REDIRECT_URL} 12 | - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} 13 | - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} 14 | - GOOGLE_REDIRECT_URL=${GOOGLE_REDIRECT_URL} 15 | command: server production 16 | --------------------------------------------------------------------------------