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.
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