documentation

Yves G 2021-09-05 17:44:46 +02:00
parent 5f44ced065
commit 20fc2dbf8b
4 changed files with 224 additions and 4 deletions

View File

@ -14,3 +14,6 @@ insert_final_newline = true
[Makefile]
indent_style = tab
tab_width = 2
[*.md]
trim_trailing_whitespace = false

217
README.md Normal file
View File

@ -0,0 +1,217 @@
# 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 “`<!--MESSAGES-->`” gets replaced at runtime with messages (if there are any), in this format:
```html
<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](./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 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:
```html
<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:
```html
<p>A substitute message.</p>
```
### 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": [ … ]}
```
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](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"
}
}
]}
```

View File

@ -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,
}

View File

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