documentation
This commit is contained in:
parent
5f44ced065
commit
20fc2dbf8b
@ -14,3 +14,6 @@ insert_final_newline = true
|
|||||||
[Makefile]
|
[Makefile]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
tab_width = 2
|
tab_width = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
217
README.md
Normal file
217
README.md
Normal 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 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 {
|
return {
|
||||||
answer_request = answer_request,
|
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,
|
set_root = set_root,
|
||||||
}
|
}
|
||||||
|
@ -92,12 +92,12 @@ local function with_sites(profile, user)
|
|||||||
if f then
|
if f then
|
||||||
site = json.decode(f:read("*all"))
|
site = json.decode(f:read("*all"))
|
||||||
f:close()
|
f:close()
|
||||||
for _, pat in ipairs(site.patterns or {}) do
|
for _, pat in ipairs(site.patterns) do
|
||||||
go_on = true
|
go_on = true
|
||||||
for _, denied in ipairs(pat.deny or {}) do
|
for _, denied in ipairs(pat.deny or {}) do
|
||||||
if denied == user then
|
if denied == user then
|
||||||
go_on = false
|
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)
|
table.insert(ko_list, re)
|
||||||
end
|
end
|
||||||
break
|
break
|
||||||
@ -175,7 +175,7 @@ local function authorized_links(user)
|
|||||||
if f then
|
if f then
|
||||||
site = json.decode(f:read("*all"))
|
site = json.decode(f:read("*all"))
|
||||||
f:close()
|
f:close()
|
||||||
for _, pat in ipairs(site.patterns or {}) do
|
for _, pat in ipairs(site.patterns) do
|
||||||
go_on = true
|
go_on = true
|
||||||
for _, denied in ipairs(pat.deny or {}) do
|
for _, denied in ipairs(pat.deny or {}) do
|
||||||
if denied == user then
|
if denied == user then
|
||||||
|
Loading…
Reference in New Issue
Block a user