diff --git a/.editorconfig b/.editorconfig index b860e67..ad1bd2a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,3 +14,6 @@ insert_final_newline = true [Makefile] indent_style = tab tab_width = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0b0243 --- /dev/null +++ b/README.md @@ -0,0 +1,219 @@ +# Simple-SSO + +This software is similar to [SSOwat](https://github.com/YunoHost/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](https://github.com/openresty/lua-nginx-module) server. +The main target is self-hosting. + +> ℹ️ [OpenResty](https://openresty.org/) is an open-source package which bundles [Nginx](http://nginx.org/) web server [with LuaJIT](https://www.nginx.com/resources/wiki/modules/lua/) and some chosen libraries. + +## Goals / features — status + +> ℹ️ Each feature is backed by [tests](./test/). + +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](https://www.rfc-editor.org/rfc/rfc7515.html) is used for authentication | Done +The [JWT](https://www.rfc-editor.org/rfc/rfc7519) 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/](./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](https://www.json.org/) structure: + +```json +{ + "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](https://www.json.org/) applies. In particular, literal characters `"` and `\` (backslash) would respectively appear as `\"` and `\\`. + +An [example `global.json` file](./doc/samples/global.json) is provided, with a command that can authenticate a user using an LDAP directory. + +### The login page + +Although [sample login files](./doc/samples/login/) 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 “``” gets replaced at runtime with messages (if there are any), in this format: + ```html +
+

An information message.

+

A warning message.

+ … +
+ ``` + * 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: + `` + or the user will always get redirected to the portal after login. + +### The portal page + +Although [sample portal files](./doc/samples/portal/) 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 user’s login identifier, name, and e-mail address, HTML-encoded; + * for this file to have any usefulness, it must contain this HTML fragment: + `` + which gets: + * _filled_ at runtime with a list of links if there are links authorized for the logged-in user: + ```html + + ``` + * or _replaced_ at runtime with a paragraph if no links are authorized for the logged-in user: + ```html +

A substitute message.

+ ``` + +### The sites’ description + +Each site is described as a [JSON object](https://www.json.org/) having one field named “`patterns`”, which is a list: + +```json +{"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](https://www.lua.org/manual/5.3/manual.html#6.4.1) that an URL must match to be considered as being in this part of the site: + +```json +{"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 user’s password, + * “`\rn.`” gets replaced by the user’s name, + * “`\re.`” gets replaced by the user’s e-mail address, + * “`\rb64(…).`” gets replaced by the [Base64 encoding](https://datatracker.ietf.org/doc/html/rfc4648#section-4) of what is inside the parentheses, + * “`\ru64(…).`” gets replaced by the [Base64 encoding with the URL variant](https://datatracker.ietf.org/doc/html/rfc4648#section-5) 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: + +```json +{"patterns": [{"lua_regex": ["/private"]}]} +``` + +And here is a fuller example: + +```json +{"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" + } + } +]} +``` diff --git a/src/ssso_login.lua b/src/ssso_login.lua index 1f4169d..3b529f3 100644 --- a/src/ssso_login.lua +++ b/src/ssso_login.lua @@ -108,6 +108,6 @@ end return { answer_request = answer_request, - check_credentials_and_get_profile = check_credentials_and_get_profile, + check_credentials_and_get_profile = check_credentials_and_get_profile, -- TODO: test set_root = set_root, } diff --git a/src/ssso_sites.lua b/src/ssso_sites.lua index c9a8091..bc64c7f 100644 --- a/src/ssso_sites.lua +++ b/src/ssso_sites.lua @@ -92,12 +92,12 @@ local function with_sites(profile, user) if f then site = json.decode(f:read("*all")) f:close() - for _, pat in ipairs(site.patterns or {}) do + for _, pat in ipairs(site.patterns) do go_on = true for _, denied in ipairs(pat.deny or {}) do if denied == user then go_on = false - for _, re in ipairs(pat.lua_regex or {}) do + for _, re in ipairs(pat.lua_regex) do table.insert(ko_list, re) end break @@ -175,7 +175,7 @@ local function authorized_links(user) if f then site = json.decode(f:read("*all")) f:close() - for _, pat in ipairs(site.patterns or {}) do + for _, pat in ipairs(site.patterns) do go_on = true for _, denied in ipairs(pat.deny or {}) do if denied == user then