# Discourse SSO in Nginx

**URL:** https://meta.discourse.org/t/discourse-sso-in-nginx/47783
**Category:** SSO
**Created:** [25 Luglio 2016, 7:39pm UTC](https://meta.discourse.org/t/discourse-sso-in-nginx/47783 "2016-07-25T19:39:12Z")
**Posts on this page:** 1
**Page:** 1

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [25 Luglio 2016, 7:39pm UTC](https://meta.discourse.org/t/discourse-sso-in-nginx/47783/1 "2016-07-25T19:39:12Z")

</div>

If you are using Nginx (or the awesome OpenResty) to handle you user authentication, you can handle the [Discourse SSO](https://meta.discourse.org/t/13045?silent=true) on the Nginx too!

This assumes you are using the ideas on this article to handle authentication:

> **[Writing an nginx authentication module in Lua - Stavros' Stuff](https://www.stavros.io/posts/writing-an-nginx-authentication-module-in-lua/)**

The following code implements the [Discourse SSO](https://meta.discourse.org/t/13045?silent=true) protocol in Lua, and even sets custom fields and the user birthday date for the [cakeday plugin](https://meta.discourse.org/t/cakeday-celebrating-birthdays-and-anniversaries/38447):

```Lua
-- First we inline this PR https://github.com/openresty/lua-resty-string/pull/18
-- You can remove this after @agentzh merges this patch by @ddragosd

-- Adds HMAC support to Lua with multiple algorithms, via OpenSSL and FFI
--
-- Author: ddragosd@gmail.com
-- Date: 16/05/14
--

local ffi = require "ffi"
local ffi_new = ffi.new
local ffi_str = ffi.string
local C = ffi.C
local resty_string = require "resty.string"
local setmetatable = setmetatable
local error = error

local _M = { _VERSION = '0.09' }

local mt = { __index = _M }

--
-- EVP_MD is defined in openssl/evp.h
-- HMAC is defined in openssl/hmac.h
--
ffi.cdef[[
typedef struct env_md_st EVP_MD;
typedef struct env_md_ctx_st EVP_MD_CTX;
unsigned char *HMAC(const EVP_MD *evp_md, const void *key, int key_len,
            const unsigned char *d, size_t n, unsigned char *md,
            unsigned int *md_len);
const EVP_MD *EVP_sha1(void);
const EVP_MD *EVP_sha224(void);
const EVP_MD *EVP_sha256(void);
const EVP_MD *EVP_sha384(void);
const EVP_MD *EVP_sha512(void);
]]

-- table definind the available algorithms and the length of each digest
-- for more information @see: http://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf
local available_algorithms = {
    sha1 = { alg = C.EVP_sha1(), length = 160/8 },
    sha224 = { alg = C.EVP_sha224(), length = 224/8 },
    sha256 = { alg = C.EVP_sha256(), length = 256/8 },
    sha384 = { alg = C.EVP_sha384(), length = 384/8 },
    sha512 = { alg = C.EVP_sha512(), length = 512/8 }
}

-- 64 is the max lenght and it covers up to sha512 algorithm
local digest_len = ffi_new("int[?]", 64)
local buf = ffi_new("char[?]", 64)

function _M.new(self)
    return setmetatable({}, mt)
end

local function getDigestAlgorithm(dtype)
    local md_name = available_algorithms[dtype]
    if ( md_name == nil ) then
        error("attempt to use unknown algorithm: '" .. dtype ..
                "'.\n Available algorithms are: sha1,sha224,sha256,sha384,sha512")
    end
    return md_name.alg, md_name.length
end

---
-- Returns the HMAC digest. The hashing algorithm is defined by the dtype parameter.
-- The optional raw flag, defaulted to false, is a boolean indicating whether the output should be a direct binary
-- equivalent of the HMAC or formatted as a hexadecimal string (the default)
--
-- @param self
-- @param dtype The hashing algorithm to use is specified by dtype
-- @param key The secret
-- @param msg The message to be signed
-- @param raw When true, it returns the binary format, else, the hex format is returned
--
function _M.digest(self, dtype, key, msg, raw)
    local evp_md, digest_length_int = getDigestAlgorithm(dtype)
    if key == nil or msg == nil then
        error("attempt to digest with a null key or message")
    end

    C.HMAC(evp_md, key, #key, msg, #msg, buf, digest_len)

    if raw == true then
        return ffi_str(buf,digest_length_int)
    end

    return resty_string.to_hex(ffi_str(buf,digest_length_int))
end

-- End of HMAC-SHA256

-- Here we start the integration

-- We assume you already have global variables with your user identification!
-- Here the user_id if avaliable on the auth_user nginx global variable set by a access_by_lua directive
-- For a how to read https://www.stavros.io/posts/writing-an-nginx-authentication-module-in-lua/
-- We are going to get more user info on a public api and implement the Discourse SSO protocol

cjson = require "cjson"
base64 = require "base64"

secret = "discoursessopassword" -- This one must match your discourse config
local args = ngx.req.get_uri_args()

local payload_raw = args["sso"]
local payload_unescaped = ngx.unescape_uri(payload_raw)
payload_decoded = base64.decode(payload_unescaped)

if args["sig"] == _M.digest(nil, "sha256", secret, payload_unescaped) then
    -- Signature matches

    local auth = ngx.location.capture("/user_details_api_endpoint?username="..ngx.var.auth_user)
    local user = cjson.decode(auth.body)

    -- Example of the response from the api
    --local user = cjson.decode("{\"start\": 1435952130436, \"elapsed\": 0, \"data\": [{\"user_id\":\"321654\",\"user_full_name\":\"RAFAEL DOS SANTOS SILVA\",\"user_position\":\"Senior Enginner\",\"birthday\":\"1987-07-13\",\"email\":\"rafael.silva@example.com\"}]}")

    local user_id = user["data"][1]["user_id"]
    local user_full_name = user["data"][1]["user_full_name"]
    local email = user["data"][1]["email"]
    local avatar = "https://example.com/avatar/"..user_id
    local user_location = user["data"][1]["user_location"]
    local user_position = user["data"][1]["user_position"]
    local birthday = user["data"][1]["birthday"]

    local response_payload = payload_decoded.."&username="..user_id.."&email="..email.."&external_id="..user_id.."&name="..user_full_name.."&custom.user_field_1="..user_location.."&custom.user_field_2="..user_position.."&custom.date_of_birth="..birthday
    local response_payload_encoded = ngx.encode_base64(response_payload)
    local response_payload_escaped = ngx.escape_uri(response_payload_encoded)
    local response_signature = _M.digest(nil, "sha256", secret, response_payload_encoded)
    ngx.redirect("https://forum.example.com/session/sso_login?sso="..response_payload_escaped.."&sig="..response_signature)
else
    -- Signature mismatch
    ngx.exit(403)
end

```

Then you create a location block like the following:

```nginx
   location /sso-forum {
          access_by_lua_file "lua/auth/access.lua"; # This file contains the code with authentication like on the stavros post and sets nginx.var.auth_user
          content_by_lua_file "lua/discourse/sso.lua"; # This file contains the code on this post
    }

```

Hope it helps someone!
