Simple Single Sign-On written in LUA and embedded into Nginx, using the OpenResty environment
 
 
 
Go to file
Yves G 12ccf169ac Merge branch 'feature/fix_deps' into develop 2023-08-18 22:25:01 +02:00
doc/samples HTTP basic auth 2021-10-03 18:27:30 +02:00
src refactor: remove code duplication 2023-08-17 18:13:13 +02:00
test replace deprecated test dependencies 2023-08-18 22:24:34 +02:00
.editorconfig documentation 2023-08-17 18:13:13 +02:00
.gitignore minimum: login, portal, redirects; todo: refactor, quality, security review 2021-10-03 18:27:30 +02:00
Makefile replace deprecated test dependencies 2023-08-18 22:24:34 +02:00
README.md documentation 2023-08-17 18:13:13 +02:00

README.md

Simple-SSO

This software is similar to SSOwat in purpose. It aims to implement a light-weight Single Sign-On layer between client HTTP software and HTTP-enabled applications hosted on a single OpenResty server.
The main target is self-hosting.

OpenResty is an open-source package which bundles Nginx web server with LuaJIT and some chosen libraries.

Goals / features — status

Each feature is backed by tests.

Goal / feature (functional) status
A new site can be handled by dropping a site-specific configuration file in a dedicated folder Done
A site can be broken-down into sub-parts by a series of Lua regular expressions to match them Done
Each part of a site has its own access rules and authentication mode Done
A site part can be public: unauthenticated visitors can access it Done
A site part can be private: only authenticated users can access it Done
A site part can accept all authenticated users or grant access to a chosen list Done
A site part, even a public one, can be forbidden to some authenticated users Done
A rejected access is redirected to the login page Done
The login page can be customized Done
On successful login, the user is sent back to the page that was aimed at before authentication Done
A portal page is available, where each site part can add its own links Done
The portal page can be customized Done
HTTP Basic Authentication can be used instead of interactive login Done
Each site can configure its logout method to reach a “Single Sign-Off” To do
Users can change their password in the identity directory To do
Goal / feature (technical) status
A JWS is used for authentication Done
The JWT is short-lived, and automatically renewed on each access Done
The JWT is self-sufficient to allow or deny access to a site Done
For better performance, the sites data is only read after a successful login (to build the first JWT), or to display the portal Done
The JWS is stored in a cookie with no impact on the integrated applications Done
The JWS can be stored in its regular header To do
Credentials and authorization data in the JWT are encrypted with 256-bit AES Done
Implementation of the OAuth2 and OpenID-Connect protocols To do
Implementation of the OpenID-Connect Discovery protocol To do
LDAP is usable as an authentication back-end Done
Any authentication back-end can be configured through shell commands Done

Installation / usage

A directory must be created (for example /etc/nginx/simplesso/) with this structure:

─┬─ login/
 │  ├─ login.html
 │  ├─ login.css
 │  └─ login.js
 ├─ portal/
 │  ├─ portal.html
 │  ├─ portal.css
 │  └─ portal.js
 ├─ sites/
 │  ├─ SITE1.json
 │  ├─ SITE2.json
 │  └─ …
 ├─ global.json
 └─ *.lua

The *.lua files are all files in the src/ source-code directory. Among these are ① do_init.lua and ② do_access.lua; these are the files that Nginx calls ① when starting, and then ② for each request, to check the access permissions.
To this end, Nginx must be configured thus (assuming the directory created above is /etc/nginx/simplesso/):

  • In the http {…} section of nginx.conf, add this line:
    init_by_lua_file /etc/nginx/simplesso/do_init.lua;
  • In the server {…} section of nginx.conf, add this line:
    access_by_lua_file /etc/nginx/simplesso/do_access.lua;

The main configuration file: global.json

The global.json file has this JSON structure:

{
  "auth": {
    "check": "AN AUTHENTICATING SHELL COMMAND"
  },
  "session_seconds": 300,
  "sso_host": "example.org",
  "sso_prefix": "/simple-sso"
}

The sso_host and sso_prefix are simply the public path to your SSO, hosted on your OpenResty server. In the above example:

  • the login page would be at https://example.org/simple-sso/login;
  • the portal page would be at https://example.org/simple-sso/portal.

The parameter session_seconds is the number of seconds (here: 5 minutes) after which a session expires in the absence of any request to the server; if there is a request, the session token is renewed, and the counter is reset.

Finally, the auth array contains the shell commands that communicate with the authentication backend. Each command can make full use of the system shell features (loops, pipes, parameter expansion…). Here are the needed commands:

  • check is the command that checks the validity of given credentials. If 2 non-empty lines can be read in the standard output of this command, then the credentials are validated, else they are rejected.
    • In the command, “\ru.” is replaced by the user identifier that was given, and “\rp.” is replaced by the password that was given. Both are properly secured for the shell: any newline character is removed, and each occurrence of “"” is escaped to become “\"”; thus “\ru.” and “\rp.” are expected to be used in double-quoted parameters in the chosen command.
    • In the output of the command, the first line is used as the user name, and the second line is used as the user email.

⚠️ Remember that the commands are written as strings in a JSON file, and thus the JSON syntax applies. In particular, literal characters " and \ (backslash) would respectively appear as \" and \\.

An example global.json file is provided, with a command that can authenticate a user using an LDAP directory.

The login page

Although sample login files are provided, each can be freely replaced, knowing these facts:

  • All three files must be encoded in UTF-8.
  • In login.html:
    • only one CSS file is allowed, and this file is login.css;
    • only one Javascript file is allowed, and this file is login.js;
    • the placeholder “<!--MESSAGES-->” gets replaced at runtime with messages (if there are any), in this format:
      <section id="messages">
        <p class="info">An information message.</p>
        <p class="warning">A warning message.</p></section>
      
    • the login action must be “login” with method POST;
    • the form parameters for the login and password must be named “login” and “password”;
    • for the redirection to work, this hidden input must be provided:
      <input type="hidden" name="back" value="BACK_URL">
      or the user will always get redirected to the portal after login.

The portal page

Although sample portal files are provided, each can be freely replaced, knowing these facts:

  • All three files must be encoded in UTF-8.
  • In portal.html:
    • only one CSS file is allowed, and this file is portal.css;
    • only one Javascript file is allowed, and this file is portal.js;
    • the placeholders “SSSO_USER”, “SSSO_NAME”, and “SSSO_EMAIL” get respectively replaced at runtime by the logged-in users login identifier, name, and e-mail address, HTML-encoded;
    • for this file to have any usefulness, it must contain this HTML fragment:
      <nav id="sites"></nav>
      which gets:
      • filled at runtime with a list of links if there are links authorized for the logged-in user:
        <nav id="sites"><ul>
          <li><a href="First authorized link"><span>Label for the first authorized link</span></a></li>
          <li><a href="Second authorized link"><span>Label for the second authorized link</span></a></li></ul></nav>
        
      • or replaced at runtime with a paragraph if no links are authorized for the logged-in user:
        <p>A substitute message.</p>
        

The sites description

Each site is described as a JSON object having one field named “patterns”, which is a list:

{"patterns": [  ]}

This list contains one JSON object per part of the site that needs to be described separately.
A part of a site contains at least the field lua_regex, that lists the possible patterns that an URL must match to be considered as being in this part of the site:

{"lua_regex": [  ]}

Sites parts are evaluated from top to bottom, so put the most specific ones first, and the “catch-all” part at the end.

In addition, the part of the site may specify:

  • public (a boolean, false by default): whether unauthenticated users may visit this part of the site;
  • allow (a list, empty by default, i.e. nobody): what authenticated users are allowed to visit this part of the site (“*” means everyone);
  • deny (a list, empty by default, i.e. nobody): what authenticated users are denied access to this part of the site;
  • actions (a list of objects, empty by default): how the single sign-on should be forwarded to the site; each object in this list has three fields:
    • type is either “cookie” (send a cookie) or “header” (send a header),
    • name is the name of the cookie or header,
    • value is the value to put in this cookie or header, where the following substitutions are done:
      • \ru.” gets replaced by the logged-in user identifier,
      • \rp.” gets replaced by the users password,
      • \rn.” gets replaced by the users name,
      • \re.” gets replaced by the users e-mail address,
      • \rb64(…).” gets replaced by the Base64 encoding of what is inside the parentheses,
      • \ru64(…).” gets replaced by the Base64 encoding with the URL variant of what is inside the parentheses;
  • portal (an object, empty by default): what links shall be added to the portal page; for each of this object:
    • the key (i.e. field name) is the URL of the link, relative to the root of the server,
    • the value is the label to show for this link.

To summarise, here is a minimal, valid but useless, example:

{"patterns": [{"lua_regex": ["/private"]}]}

And here is a fuller example:

{"patterns": [
  { "lua_regex": ["/site/assets", "/site/wiki"], "public": true, "allow": ["*"],
    "actions": [
      {"type": "header", "name": "X-WIKI-USER", "value": "\rn."},
      {"type": "header", "name": "X-WIKI-MAIL", "value": "\re."}
    ],
    "portal": {
      "/site/wiki/overview.html": "Wiki pages"
    }
  },
  {"lua_regex": ["/site/admin"], "allow": ["*"], "deny": ["former-administrator"],
    "actions": [
      {"type": "header", "name": "Authorization", "value": "Basic \rb64(\ru.:\rp.)."}
    ],
    "portal": {
      "/site/admin/plugins.php": "Wiki plugins",
      "/site/admin/statistics.php": "Wiki visitors statistics"
    }
  }
]}