documentation
parent
5f44ced065
commit
20fc2dbf8b
|
@ -14,3 +14,6 @@ insert_final_newline = true
|
|||
[Makefile]
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
|
|
@ -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 user’s 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 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"
|
||||
}
|
||||
}
|
||||
]}
|
||||
```
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue