mirror of
https://github.com/jech/galene.git
synced 2024-11-22 08:35:57 +01:00
Implement token authentication.
This commit is contained in:
parent
b4d1ef398f
commit
03811db37d
12 changed files with 433 additions and 28 deletions
88
README
88
README
|
@ -91,9 +91,10 @@ optional, but unless you specify at least one user definition (`op`,
|
||||||
following fields are allowed:
|
following fields are allowed:
|
||||||
|
|
||||||
- `op`, `presenter`, `other`: each of these is an array of user
|
- `op`, `presenter`, `other`: each of these is an array of user
|
||||||
definitions (see below) and specifies the users allowed to connect
|
definitions (see *Authorisation* below) and specifies the users allowed
|
||||||
respectively with operator privileges, with presenter privileges, and
|
to connect respectively with operator privileges, with presenter
|
||||||
as passive listeners;
|
privileges, and as passive listeners;
|
||||||
|
- `authServer` and `authKeys`: see *Authorisation* below;
|
||||||
- `public`: if true, then the group is visible on the landing page;
|
- `public`: if true, then the group is visible on the landing page;
|
||||||
- `displayName`: a human-friendly version of the group name;
|
- `displayName`: a human-friendly version of the group name;
|
||||||
- `description`: a human-readable description of the group; this is
|
- `description`: a human-readable description of the group; this is
|
||||||
|
@ -131,31 +132,53 @@ Supported video codecs include:
|
||||||
Supported audio codecs include `"opus"`, `"g722"`, `"pcmu"` and `"pcma"`.
|
Supported audio codecs include `"opus"`, `"g722"`, `"pcmu"` and `"pcma"`.
|
||||||
Only Opus can be recorded to disk. There is no good reason to use
|
Only Opus can be recorded to disk. There is no good reason to use
|
||||||
anything except Opus.
|
anything except Opus.
|
||||||
|
|
||||||
A user definition is a dictionary with the following fields:
|
|
||||||
|
|
||||||
- `username`: the username of the user; if omitted, any username is
|
|
||||||
allowed;
|
## Client Authorisation
|
||||||
- `password`: if omitted, then no password is required. Otherwise, this
|
|
||||||
can either be a string, specifying a plain text password, or
|
Galene implements two authorisation methods: a simple username/password
|
||||||
a dictionary generated by the `galene-password-generator` utility.
|
authorisation scheme that is built into the Galene server, and
|
||||||
|
a token-based mechanism that relies on an external server. The simple
|
||||||
For example,
|
mechanism is intended to be used in standalone installations, while the
|
||||||
|
server-based mechanism is designed to allow easy integration with an
|
||||||
|
existing authorisation infrastructure (such as LDAP, OAuth2, or even Unix
|
||||||
|
passwords).
|
||||||
|
|
||||||
|
### Password authorisation
|
||||||
|
|
||||||
|
When password authorisation is used, authorised usernames and password are
|
||||||
|
defined directly in the group configuration file, in the `op`, `presenter`
|
||||||
|
and `other` arrays. Each member of the array is a dictionary, that may
|
||||||
|
contain the fields `username` and `password`:
|
||||||
|
|
||||||
|
- if `username` is present, then the entry only matches clients that
|
||||||
|
specify this exact username; otherwise, any username matches;
|
||||||
|
- if `password` is present, then the entry only matches clients that
|
||||||
|
specify this exact password; otherwise, any password matches.
|
||||||
|
|
||||||
|
For example, the entry
|
||||||
|
|
||||||
{"username": "jch", "password": "1234"}
|
{"username": "jch", "password": "1234"}
|
||||||
|
|
||||||
specifies user *jch* with password *1234*, while
|
specifies username *jch* with password *1234*, while
|
||||||
|
|
||||||
{"password": "1234"}
|
{"password": "1234"}
|
||||||
|
|
||||||
specifies that any (non-empty) username will do, and
|
allows any username with password *1234*, and
|
||||||
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
allows any (non-empty) username with any password.
|
allows any username with any password.
|
||||||
|
|
||||||
|
By default, empty usernames are forbidden; set the `allow-anonymous`
|
||||||
|
option to allow empty usernames. By default, recording is forbidden;
|
||||||
|
specify the `allow-recording` option to allow operators to record.
|
||||||
|
|
||||||
|
|
||||||
|
### Hashed passwords
|
||||||
|
|
||||||
If you don't wish to store cleartext passwords on the server, you may
|
If you don't wish to store cleartext passwords on the server, you may
|
||||||
generate hashed password with the `galene-password-generator` utility. A
|
generate hashed passwords with the `galene-password-generator` utility. A
|
||||||
user entry with a hashed password looks like this:
|
user entry with a hashed password looks like this:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -170,6 +193,39 @@ user entry with a hashed password looks like this:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### Authorisation servers
|
||||||
|
|
||||||
|
Galene is able to delegate authorisation decisions to an external
|
||||||
|
authorisation server. This makes it possible to integrate Galene with an
|
||||||
|
existing authentication and authorisation infrastructure, such as LDAP,
|
||||||
|
OAuth2 or even Unix passwords.
|
||||||
|
|
||||||
|
When an authorisation server is used, the group configuration file
|
||||||
|
specifies the URL of the authorisation server and one or more public keys
|
||||||
|
in JWK format:
|
||||||
|
|
||||||
|
{
|
||||||
|
"authServer": "https://auth.example.org",
|
||||||
|
"authKeys": [{
|
||||||
|
"kty": "oct",
|
||||||
|
"alg": "HS256",
|
||||||
|
"k": "MYz3IfCq4Yq-UmPdNqWEOdPl4C_m9imHHs9uveDUJGQ",
|
||||||
|
"kid": "20211030"
|
||||||
|
}, {
|
||||||
|
"kty": "EC",
|
||||||
|
"alg": "ES256",
|
||||||
|
"crv": "P-256",
|
||||||
|
"x": "dElK9qBNyCpRXdvJsn4GdjrFzScSzpkz_I0JhKbYC88",
|
||||||
|
"y": "pBhVb37haKvwEoleoW3qxnT4y5bK35_RTP7_RmFKR6Q",
|
||||||
|
"kid": "20211101"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
The `kid` field serves to distinguish among multiple keys, and must match
|
||||||
|
the value provided by the authorisation server. If the server doesn't
|
||||||
|
provide a `kid`, the first key with a matching `alg` field will be used.
|
||||||
|
|
||||||
|
|
||||||
# Further information
|
# Further information
|
||||||
|
|
||||||
Galène's web page is at <https://galene.org>.
|
Galène's web page is at <https://galene.org>.
|
||||||
|
|
|
@ -39,6 +39,11 @@ configuration file. Each object has the following fields:
|
||||||
- `locked`: true if the group is locked;
|
- `locked`: true if the group is locked;
|
||||||
- `clientCount`: the number of clients currently in the group.
|
- `clientCount`: the number of clients currently in the group.
|
||||||
|
|
||||||
|
If token-based authorisation is in use for the group, then the dictionary
|
||||||
|
contains the following additional field:
|
||||||
|
|
||||||
|
- `authServer`: the URL of the authorisation server.
|
||||||
|
|
||||||
A client may also fetch the URL `/group/name/.status.json` to retrieve the
|
A client may also fetch the URL `/group/name/.status.json` to retrieve the
|
||||||
status of a single group. If the group has not been marked as public,
|
status of a single group. If the group has not been marked as public,
|
||||||
then the fields `locked` and `clientCount` are omitted.
|
then the fields `locked` and `clientCount` are omitted.
|
||||||
|
@ -112,6 +117,9 @@ The `join` message requests that the sender join or leave a group:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If token-based authorisation is beling used, then the `password` field is
|
||||||
|
omitted, and a `token` field is included instead.
|
||||||
|
|
||||||
When the sender has effectively joined the group, the peer will send
|
When the sender has effectively joined the group, the peer will send
|
||||||
a 'joined' message of kind 'join'; it may then send a 'joined' message of
|
a 'joined' message of kind 'join'; it may then send a 'joined' message of
|
||||||
kind 'change' at any time, in order to inform the client of a change in
|
kind 'change' at any time, in order to inform the client of a change in
|
||||||
|
@ -355,3 +363,37 @@ Finally, a group action requests that the server act on the current group.
|
||||||
Currently defined kinds include `clearchat` (not to be confused with the
|
Currently defined kinds include `clearchat` (not to be confused with the
|
||||||
`clearchat` user message), `lock`, `unlock`, `record`, `unrecord`,
|
`clearchat` user message), `lock`, `unlock`, `record`, `unrecord`,
|
||||||
`subgroups` and `setdata`.
|
`subgroups` and `setdata`.
|
||||||
|
|
||||||
|
# Authorisation protocol
|
||||||
|
|
||||||
|
If a group's status dictionary has a non-empty `authServer` field, then
|
||||||
|
the group uses token authentication. Before joining, the client sends
|
||||||
|
a POST request to the authorisation server URL containing in its body
|
||||||
|
a JSON dictionary of the following form:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"location": "https://galene.example.org/group/groupname",
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
}
|
||||||
|
```
|
||||||
|
If the user is not allowed to join the group, then the authorisation
|
||||||
|
server replies with a code of 403 ("not authorised"). If the user is
|
||||||
|
allowed to join, then the authorisation server replies with a signed JWT
|
||||||
|
(a "JWS") the body of which has the following form:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"sub": username,
|
||||||
|
"aud": "https://galene.example.org/group/groupname",
|
||||||
|
"permissions": ["present": true],
|
||||||
|
"iat": now,
|
||||||
|
"exp": now + 30s,
|
||||||
|
"iss": authorisation server URL
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The `permissions` field contains the permissions granted to the client, in
|
||||||
|
the same format as in the `joined` message. Since the client will only
|
||||||
|
use the token once, at the very beginning of the session, the tokens
|
||||||
|
issued may have a short lifetime (on the order of 30s).
|
||||||
|
|
||||||
|
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -1,9 +1,10 @@
|
||||||
module github.com/jech/galene
|
module github.com/jech/galene
|
||||||
|
|
||||||
go 1.13
|
go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/at-wat/ebml-go v0.16.0
|
github.com/at-wat/ebml-go v0.16.0
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.2.0
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/jech/cert v0.0.0-20210819231831-aca735647728
|
github.com/jech/cert v0.0.0-20210819231831-aca735647728
|
||||||
github.com/jech/samplebuilder v0.0.0-20220125212352-4553ed6f9a6c
|
github.com/jech/samplebuilder v0.0.0-20220125212352-4553ed6f9a6c
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
|
|
@ -92,6 +92,7 @@ type ClientCredentials struct {
|
||||||
System bool
|
System bool
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -15,6 +16,8 @@ import (
|
||||||
"github.com/pion/ice/v2"
|
"github.com/pion/ice/v2"
|
||||||
"github.com/pion/sdp/v3"
|
"github.com/pion/sdp/v3"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
|
|
||||||
|
"github.com/jech/galene/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Directory, DataDirectory string
|
var Directory, DataDirectory string
|
||||||
|
@ -960,6 +963,12 @@ type Description struct {
|
||||||
// A list of logins for non-presenting users.
|
// A list of logins for non-presenting users.
|
||||||
Other []ClientPattern `json:"other,omitempty"`
|
Other []ClientPattern `json:"other,omitempty"`
|
||||||
|
|
||||||
|
// The URL of the authentication server.
|
||||||
|
AuthServer string `json:"authServer"`
|
||||||
|
|
||||||
|
// The (public) keys of the authentication server
|
||||||
|
AuthKeys []map[string]interface{} `json:"authKeys"`
|
||||||
|
|
||||||
// Codec preferences. If empty, a suitable default is chosen in
|
// Codec preferences. If empty, a suitable default is chosen in
|
||||||
// the APIFromNames function.
|
// the APIFromNames function.
|
||||||
Codecs []string `json:"codecs,omitempty"`
|
Codecs []string `json:"codecs,omitempty"`
|
||||||
|
@ -1062,6 +1071,7 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) (C
|
||||||
if !desc.AllowAnonymous && creds.Username == "" {
|
if !desc.AllowAnonymous && creds.Username == "" {
|
||||||
return p, ErrAnonymousNotAuthorised
|
return p, ErrAnonymousNotAuthorised
|
||||||
}
|
}
|
||||||
|
|
||||||
if found, good := matchClient(group, creds, desc.Op); found {
|
if found, good := matchClient(group, creds, desc.Op); found {
|
||||||
if good {
|
if good {
|
||||||
p.Op = true
|
p.Op = true
|
||||||
|
@ -1086,6 +1096,52 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) (C
|
||||||
}
|
}
|
||||||
return p, ErrNotAuthorised
|
return p, ErrNotAuthorised
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if desc.AuthServer != "" && creds.Token != "" {
|
||||||
|
aud, perms, err := token.Valid(
|
||||||
|
creds.Username, creds.Token,
|
||||||
|
desc.AuthKeys, desc.AuthServer,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Token authentication: %v", err)
|
||||||
|
return p, ErrNotAuthorised
|
||||||
|
}
|
||||||
|
conf, err := GetConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Read config.json: %v", err)
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
ok := false
|
||||||
|
for _, u := range aud {
|
||||||
|
url, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Token URL: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// if canonicalHost is not set, we allow tokens
|
||||||
|
// for any domain name. Hopefully different
|
||||||
|
// servers use distinct keys.
|
||||||
|
if conf.CanonicalHost != "" {
|
||||||
|
if !strings.EqualFold(
|
||||||
|
url.Host, conf.CanonicalHost,
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if url.Path == path.Join("/group", group)+"/" {
|
||||||
|
ok = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return p, ErrNotAuthorised
|
||||||
|
}
|
||||||
|
p.Op, _ = perms["op"].(bool)
|
||||||
|
p.Present, _ = perms["present"].(bool)
|
||||||
|
p.Record, _ = perms["record"].(bool)
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
return p, ErrNotAuthorised
|
return p, ErrNotAuthorised
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1093,6 +1149,7 @@ type Status struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
DisplayName string `json:"displayName,omitempty"`
|
DisplayName string `json:"displayName,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
|
AuthServer string `json:"authServer,omitempty"`
|
||||||
Locked bool `json:"locked,omitempty"`
|
Locked bool `json:"locked,omitempty"`
|
||||||
ClientCount *int `json:"clientCount,omitempty"`
|
ClientCount *int `json:"clientCount,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -1102,6 +1159,7 @@ func (g *Group) Status (authentified bool) Status {
|
||||||
d := Status{
|
d := Status{
|
||||||
Name: g.name,
|
Name: g.name,
|
||||||
DisplayName: desc.DisplayName,
|
DisplayName: desc.DisplayName,
|
||||||
|
AuthServer: desc.AuthServer,
|
||||||
Description: desc.Description,
|
Description: desc.Description,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,7 @@ type clientMessage struct {
|
||||||
Dest string `json:"dest,omitempty"`
|
Dest string `json:"dest,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
Privileged bool `json:"privileged,omitempty"`
|
Privileged bool `json:"privileged,omitempty"`
|
||||||
Permissions *group.ClientPermissions `json:"permissions,omitempty"`
|
Permissions *group.ClientPermissions `json:"permissions,omitempty"`
|
||||||
Status *group.Status `json:"status,omitempty"`
|
Status *group.Status `json:"status,omitempty"`
|
||||||
|
@ -1332,6 +1333,7 @@ func handleClientMessage(c *webClient, m clientMessage) error {
|
||||||
group.ClientCredentials{
|
group.ClientCredentials{
|
||||||
Username: m.Username,
|
Username: m.Username,
|
||||||
Password: m.Password,
|
Password: m.Password,
|
||||||
|
Token: m.Token,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -271,13 +271,25 @@ function setConnected(connected) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @this {ServerConnection} */
|
/** @this {ServerConnection} */
|
||||||
function gotConnected() {
|
async function gotConnected() {
|
||||||
username = getInputElement('username').value.trim();
|
username = getInputElement('username').value.trim();
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
|
|
||||||
|
let pw = getInputElement('password').value;
|
||||||
|
getInputElement('password').value = '';
|
||||||
|
let credentials;
|
||||||
|
if(!groupStatus.authServer)
|
||||||
|
credentials = pw;
|
||||||
|
else
|
||||||
|
credentials = {
|
||||||
|
type: 'authServer',
|
||||||
|
authServer: groupStatus.authServer,
|
||||||
|
location: location.href,
|
||||||
|
password: pw,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let pw = getInputElement('password').value;
|
await this.join(group, username, credentials);
|
||||||
getInputElement('password').value = '';
|
|
||||||
this.join(group, username, pw);
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
displayError(e);
|
displayError(e);
|
||||||
|
|
|
@ -205,6 +205,7 @@ function ServerConnection() {
|
||||||
* @property {string} [dest]
|
* @property {string} [dest]
|
||||||
* @property {string} [username]
|
* @property {string} [username]
|
||||||
* @property {string} [password]
|
* @property {string} [password]
|
||||||
|
* @property {string} [token]
|
||||||
* @property {boolean} [privileged]
|
* @property {boolean} [privileged]
|
||||||
* @property {Object<string,boolean>} [permissions]
|
* @property {Object<string,boolean>} [permissions]
|
||||||
* @property {Object<string,any>} [status]
|
* @property {Object<string,any>} [status]
|
||||||
|
@ -416,19 +417,52 @@ ServerConnection.prototype.connect = async function(url) {
|
||||||
*
|
*
|
||||||
* @param {string} group - The name of the group to join.
|
* @param {string} group - The name of the group to join.
|
||||||
* @param {string} username - the username to join as.
|
* @param {string} username - the username to join as.
|
||||||
* @param {string} password - the password.
|
* @param {string|Object} credentials - password or authServer.
|
||||||
* @param {Object<string,any>} [data] - the initial associated data.
|
* @param {Object<string,any>} [data] - the initial associated data.
|
||||||
*/
|
*/
|
||||||
ServerConnection.prototype.join = function(group, username, password, data) {
|
ServerConnection.prototype.join = async function(group, username, credentials, data) {
|
||||||
let m = {
|
let m = {
|
||||||
type: 'join',
|
type: 'join',
|
||||||
kind: 'join',
|
kind: 'join',
|
||||||
group: group,
|
group: group,
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
|
||||||
};
|
};
|
||||||
|
if((typeof credentials) === 'string') {
|
||||||
|
m.password = credentials;
|
||||||
|
} else {
|
||||||
|
switch(credentials.type) {
|
||||||
|
case 'password':
|
||||||
|
m.password = credentials.password;
|
||||||
|
break;
|
||||||
|
case 'token':
|
||||||
|
m.token = credentials.token;
|
||||||
|
break;
|
||||||
|
case 'authServer':
|
||||||
|
let r = await fetch(credentials.authServer, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
location: credentials.location,
|
||||||
|
username: username,
|
||||||
|
password: credentials.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if(!r.ok)
|
||||||
|
throw new Error(
|
||||||
|
`The authorisation server said: ${r.status} ${r.statusText}`,
|
||||||
|
);
|
||||||
|
m.token = await r.text();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown credentials type ${credentials.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(data)
|
if(data)
|
||||||
m.data = data;
|
m.data = data;
|
||||||
|
|
||||||
this.send(m);
|
this.send(m);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
138
token/token.go
Normal file
138
token/token.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseBase64(k string, d map[string]interface{}) ([]byte, error) {
|
||||||
|
v, ok := d[k].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("key " + k + " not found")
|
||||||
|
}
|
||||||
|
vv, err := base64.RawURLEncoding.DecodeString(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return vv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKey(key map[string]interface{}) (interface{}, error) {
|
||||||
|
kty, ok := key["kty"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("kty not found")
|
||||||
|
}
|
||||||
|
alg, ok := key["alg"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("alg not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kty {
|
||||||
|
case "oct":
|
||||||
|
var length int
|
||||||
|
switch(alg) {
|
||||||
|
case "HS256":
|
||||||
|
length = 32
|
||||||
|
case "HS384":
|
||||||
|
length = 48
|
||||||
|
case "HS512":
|
||||||
|
length = 64
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unknown alg")
|
||||||
|
}
|
||||||
|
k, err := parseBase64("k", key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(k) != length {
|
||||||
|
return nil, errors.New("bad length for key")
|
||||||
|
}
|
||||||
|
return k, nil
|
||||||
|
case "EC":
|
||||||
|
if alg != "ES256" {
|
||||||
|
return nil, errors.New("uknown alg")
|
||||||
|
}
|
||||||
|
crv, ok := key["crv"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("crv not found")
|
||||||
|
}
|
||||||
|
if crv != "P-256" {
|
||||||
|
return nil, errors.New("unknown crv")
|
||||||
|
}
|
||||||
|
curve := elliptic.P256()
|
||||||
|
xbytes, err := parseBase64("x", key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var x big.Int
|
||||||
|
x.SetBytes(xbytes)
|
||||||
|
ybytes, err := parseBase64("y", key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var y big.Int
|
||||||
|
y.SetBytes(ybytes)
|
||||||
|
if !curve.IsOnCurve(&x, &y) {
|
||||||
|
return nil, errors.New("key is not on curve")
|
||||||
|
}
|
||||||
|
return &ecdsa.PublicKey{
|
||||||
|
Curve: curve,
|
||||||
|
X: &x,
|
||||||
|
Y: &y,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unknown key type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKey(header map[string]interface{}, keys []map[string]interface{}) (interface{}, error) {
|
||||||
|
alg, _ := header["alg"].(string)
|
||||||
|
kid, _ := header["kid"].(string)
|
||||||
|
for _, k := range keys {
|
||||||
|
kid2, _ := k["kid"].(string)
|
||||||
|
alg2, _ := k["alg"].(string)
|
||||||
|
if (kid == "" || kid == kid2) && alg == alg2 {
|
||||||
|
return parseKey(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("key not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Valid(username, token string, keys []map[string]interface{}, issuer string) ([]string, map[string]interface{}, error) {
|
||||||
|
tok, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
return getKey(t.Header, keys)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
claims := tok.Claims.(jwt.MapClaims)
|
||||||
|
|
||||||
|
sub, ok := claims["sub"].(string)
|
||||||
|
if !ok || sub != username {
|
||||||
|
return nil, nil, errors.New("invalid 'sub' field")
|
||||||
|
}
|
||||||
|
iss, ok := claims["iss"].(string)
|
||||||
|
if !ok || iss != issuer {
|
||||||
|
return nil, nil, errors.New("invalid 'iss' field")
|
||||||
|
}
|
||||||
|
aud, ok := claims["aud"]
|
||||||
|
var res []string
|
||||||
|
if ok {
|
||||||
|
switch aud := aud.(type) {
|
||||||
|
case string:
|
||||||
|
res = []string{aud}
|
||||||
|
case []string:
|
||||||
|
res = aud
|
||||||
|
}
|
||||||
|
}
|
||||||
|
perms, ok := claims["permissions"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.New("invalid 'permissions' field")
|
||||||
|
}
|
||||||
|
return res, perms, nil
|
||||||
|
}
|
54
token/token_test.go
Normal file
54
token/token_test.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHS256(t *testing.T) {
|
||||||
|
key := `{
|
||||||
|
"kty":"oct",
|
||||||
|
"alg":"HS256",
|
||||||
|
"k":"4S9YZLHK1traIaXQooCnPfBw_yR8j9VEPaAMWAog_YQ"
|
||||||
|
}`
|
||||||
|
var j map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(key), &j)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
k, err := parseKey(j)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseKey: %v", err)
|
||||||
|
}
|
||||||
|
kk, ok := k.([]byte)
|
||||||
|
if !ok || len(kk) != 32 {
|
||||||
|
t.Errorf("parseKey: got %v", kk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestES256(t *testing.T) {
|
||||||
|
key := `{
|
||||||
|
"kty":"EC",
|
||||||
|
"alg":"ES256",
|
||||||
|
"crv":"P-256",
|
||||||
|
"x":"dElK9qBNyCpRXdvJsn4GdjrFzScSzpkz_I0JhKbYC88",
|
||||||
|
"y":"pBhVb37haKvwEoleoW3qxnT4y5bK35_RTP7_RmFKR6Q"
|
||||||
|
}`
|
||||||
|
var j map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(key), &j)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
k, err := parseKey(j)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseKey: %v", err)
|
||||||
|
}
|
||||||
|
kk, ok := k.(*ecdsa.PublicKey)
|
||||||
|
if !ok || kk.Params().Name != "P-256" {
|
||||||
|
t.Errorf("parseKey: got %v", kk)
|
||||||
|
}
|
||||||
|
if !kk.IsOnCurve(kk.X, kk.Y) {
|
||||||
|
t.Errorf("point is not on curve")
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,9 +86,13 @@ func Serve(address string, dataDir string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func cspHeader(w http.ResponseWriter) {
|
func cspHeader(w http.ResponseWriter, connect string) {
|
||||||
|
c := "connect-src ws: wss: 'self';"
|
||||||
|
if connect != "" {
|
||||||
|
c = "connect-src " + connect + " ws: wss: 'self';"
|
||||||
|
}
|
||||||
w.Header().Add("Content-Security-Policy",
|
w.Header().Add("Content-Security-Policy",
|
||||||
"connect-src ws: wss: 'self'; img-src data: 'self'; media-src blob: 'self'; default-src 'self'")
|
c + " img-src data: 'self'; media-src blob: 'self'; default-src 'self'")
|
||||||
}
|
}
|
||||||
|
|
||||||
func notFound(w http.ResponseWriter) {
|
func notFound(w http.ResponseWriter) {
|
||||||
|
@ -172,7 +176,7 @@ func (fh *fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cspHeader(w)
|
cspHeader(w, "")
|
||||||
p := r.URL.Path
|
p := r.URL.Path
|
||||||
// this ensures any leading .. are removed by path.Clean below
|
// this ensures any leading .. are removed by path.Clean below
|
||||||
if !strings.HasPrefix(p, "/") {
|
if !strings.HasPrefix(p, "/") {
|
||||||
|
@ -314,7 +318,8 @@ func groupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cspHeader(w)
|
status := g.Status(false)
|
||||||
|
cspHeader(w, status.AuthServer)
|
||||||
serveFile(w, r, filepath.Join(StaticRoot, "galene.html"))
|
serveFile(w, r, filepath.Join(StaticRoot, "galene.html"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue