Commit de3a016f authored by Juliusz Chroboczek's avatar Juliusz Chroboczek

Set the username in the server when using tokens.

This avoids the need to pass the username in the URL without
requiring the client to parse tokens.
parent c4d46d20
...@@ -117,8 +117,8 @@ The `join` message requests that the sender join or leave a group: ...@@ -117,8 +117,8 @@ The `join` message requests that the sender join or leave a group:
} }
``` ```
If token-based authorisation is beling used, then the `password` field is If token-based authorisation is beling used, then the `username` and
omitted, and a `token` field is included instead. `password` fields are 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
...@@ -138,10 +138,11 @@ its permissions or in the recommended RTC configuration. ...@@ -138,10 +138,11 @@ its permissions or in the recommended RTC configuration.
} }
``` ```
The `permissions` field is an array of strings that may contain the values The `username` field is the username that the server assigned to this
`present`, `op` and `record`. The `status` field is a dictionary that user. The `permissions` field is an array of strings that may contain the
contains status information about the group, in the same format as at the values `present`, `op` and `record`. The `status` field is a dictionary
`.status.json` URL above. that contains status information about the group, in the same format as at
the `.status.json` URL above.
## Maintaining group membership ## Maintaining group membership
...@@ -366,13 +367,24 @@ Currently defined kinds include `clearchat` (not to be confused with the ...@@ -366,13 +367,24 @@ Currently defined kinds include `clearchat` (not to be confused with the
# Authorisation protocol # Authorisation protocol
In addition to username/password authentication, Galene supports
authentication using cryptographic tokens. Two flows are supported: using
an authentication server, where Galene's client requests a token from
a third-party server, and using an authentication portal, where
a third-party login portal redirects the user to Galene. Authentication
servers are somewhat simpler to implement, but authentication portals are
more flexible and avoid communicating the user's password to Galene's
Javascript code.
## Authentication server
If a group's status dictionary has a non-empty `authServer` field, then If a group's status dictionary has a non-empty `authServer` field, then
the group uses token authentication. Before joining, the client sends the group uses an authentication server. Before joining, the client sends
a POST request to the authorisation server URL containing in its body a POST request to the authorisation server URL containing in its body
a JSON dictionary of the following form: a JSON dictionary of the following form:
```javascript ```javascript
{ {
"location": "https://galene.example.org/group/groupname", "location": "https://galene.example.org/group/groupname/",
"username": username, "username": username,
"password": password "password": password
} }
...@@ -384,7 +396,7 @@ allowed to join, then the authorisation server replies with a signed JWT ...@@ -384,7 +396,7 @@ allowed to join, then the authorisation server replies with a signed JWT
```javascript ```javascript
{ {
"sub": username, "sub": username,
"aud": "https://galene.example.org/group/groupname", "aud": "https://galene.example.org/group/groupname/",
"permissions": ["present"], "permissions": ["present"],
"iat": now, "iat": now,
"exp": now + 30s, "exp": now + 30s,
...@@ -396,4 +408,12 @@ the same format as in the `joined` message. Since the client will only ...@@ -396,4 +408,12 @@ 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 use the token once, at the very beginning of the session, the tokens
issued may have a short lifetime (on the order of 30s). issued may have a short lifetime (on the order of 30s).
## Authentication portal
If a group's status dictionary has a non-empty `authPortal` field, Galene
redirects the user agent to the URL indicated by `authPortal`. The
authentication portal performs authorisation, generates a token as above,
then redirects back to the group's URL with the token stores in a URL
query parameter named `token`:
https://galene.example.org/group/groupname/?token=eyJhbG...
...@@ -59,6 +59,10 @@ func (client *Client) Username() string { ...@@ -59,6 +59,10 @@ func (client *Client) Username() string {
return "RECORDING" return "RECORDING"
} }
func (client *Client) SetUsername(string) {
return
}
func (client *Client) SetPermissions(perms []string) { func (client *Client) SetPermissions(perms []string) {
return return
} }
......
...@@ -100,9 +100,6 @@ func main() { ...@@ -100,9 +100,6 @@ func main() {
fmt.Println(s) fmt.Println(s)
} else { } else {
query := url.Values{} query := url.Values{}
if username != "" {
query.Add("username", username)
}
query.Add("token", s) query.Add("token", s)
outURL := &url.URL{ outURL := &url.URL{
Scheme: groupURL.Scheme, Scheme: groupURL.Scheme,
......
...@@ -92,6 +92,7 @@ type Client interface { ...@@ -92,6 +92,7 @@ type Client interface {
Group() *Group Group() *Group
Id() string Id() string
Username() string Username() string
SetUsername(string)
Permissions() []string Permissions() []string
SetPermissions([]string) SetPermissions([]string)
Data() map[string]interface{} Data() map[string]interface{}
......
...@@ -565,11 +565,12 @@ func AddClient(group string, c Client, creds ClientCredentials) (*Group, error) ...@@ -565,11 +565,12 @@ func AddClient(group string, c Client, creds ClientCredentials) (*Group, error)
clients := g.getClientsUnlocked(nil) clients := g.getClientsUnlocked(nil)
if !member("system", c.Permissions()) { if !member("system", c.Permissions()) {
perms, err := g.description.GetPermission(group, creds) username, perms, err := g.description.GetPermission(group, creds)
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.SetUsername(username)
c.SetPermissions(perms) c.SetPermissions(perms)
if !member("op", perms) { if !member("op", perms) {
...@@ -1078,9 +1079,9 @@ func GetDescription(name string) (*Description, error) { ...@@ -1078,9 +1079,9 @@ func GetDescription(name string) (*Description, error) {
return &desc, nil return &desc, nil
} }
func (desc *Description) GetPermission(group string, creds ClientCredentials) ([]string, error) { func (desc *Description) GetPermission(group string, creds ClientCredentials) (string, []string, error) {
if !desc.AllowAnonymous && creds.Username == "" { if !desc.AllowAnonymous && creds.Username == "" {
return nil, ErrAnonymousNotAuthorised return "", nil, ErrAnonymousNotAuthorised
} }
if creds.Token == "" { if creds.Token == "" {
...@@ -1091,36 +1092,34 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) ([ ...@@ -1091,36 +1092,34 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) ([
if desc.AllowRecording { if desc.AllowRecording {
p = append(p, "record") p = append(p, "record")
} }
return p, nil return creds.Username, p, nil
} }
return nil, ErrNotAuthorised return "", nil, ErrNotAuthorised
} }
if found, good := matchClient(group, creds, desc.Presenter); found { if found, good := matchClient(group, creds, desc.Presenter); found {
if good { if good {
return []string{"present"}, nil return creds.Username, []string{"present"}, nil
} }
return nil, ErrNotAuthorised return "", nil, ErrNotAuthorised
} }
if found, good := matchClient(group, creds, desc.Other); found { if found, good := matchClient(group, creds, desc.Other); found {
if good { if good {
return nil, nil return creds.Username, nil, nil
} }
return nil, ErrNotAuthorised return "", nil, ErrNotAuthorised
} }
return nil, ErrNotAuthorised return "", nil, ErrNotAuthorised
} }
aud, perms, err := token.Valid( sub, aud, perms, err := token.Valid(creds.Token, desc.AuthKeys)
creds.Username, creds.Token, desc.AuthKeys,
)
if err != nil { if err != nil {
log.Printf("Token authentication: %v", err) log.Printf("Token authentication: %v", err)
return nil, ErrNotAuthorised return "", nil, ErrNotAuthorised
} }
conf, err := GetConfiguration() conf, err := GetConfiguration()
if err != nil { if err != nil {
log.Printf("Read config.json: %v", err) log.Printf("Read config.json: %v", err)
return nil, err return "", nil, err
} }
ok := false ok := false
for _, u := range aud { for _, u := range aud {
...@@ -1145,9 +1144,9 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) ([ ...@@ -1145,9 +1144,9 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) ([
} }
} }
if !ok { if !ok {
return nil, ErrNotAuthorised return "", nil, ErrNotAuthorised
} }
return perms, nil return sub, perms, nil
} }
type Status struct { type Status struct {
......
...@@ -167,7 +167,7 @@ func TestPermissions(t *testing.T) { ...@@ -167,7 +167,7 @@ func TestPermissions(t *testing.T) {
for _, c := range badClients { for _, c := range badClients {
t.Run("bad "+c.Username, func(t *testing.T) { t.Run("bad "+c.Username, func(t *testing.T) {
p, err := d.GetPermission("test", c) _, p, err := d.GetPermission("test", c)
if err != ErrNotAuthorised { if err != ErrNotAuthorised {
t.Errorf("GetPermission %v: %v %v", c, err, p) t.Errorf("GetPermission %v: %v %v", c, err, p)
} }
...@@ -176,12 +176,13 @@ func TestPermissions(t *testing.T) { ...@@ -176,12 +176,13 @@ func TestPermissions(t *testing.T) {
for _, cp := range goodClients { for _, cp := range goodClients {
t.Run("good "+cp.c.Username, func(t *testing.T) { t.Run("good "+cp.c.Username, func(t *testing.T) {
p, err := d.GetPermission("test", cp.c) u, p, err := d.GetPermission("test", cp.c)
if err != nil { if err != nil {
t.Errorf("GetPermission %v: %v", cp.c, err) t.Errorf("GetPermission %v: %v", cp.c, err)
} else if !reflect.DeepEqual(p, cp.p) { } else if u != cp.c.Username ||
t.Errorf("%v: got %v, expected %v", !reflect.DeepEqual(p, cp.p) {
cp.c, p, cp.p) t.Errorf("%v: got %v %v, expected %v",
cp.c, u, p, cp.p)
} }
}) })
} }
......
...@@ -86,6 +86,10 @@ func (c *webClient) Username() string { ...@@ -86,6 +86,10 @@ func (c *webClient) Username() string {
return c.username return c.username
} }
func (c *webClient) SetUsername(username string) {
c.username = username
}
func (c *webClient) Permissions() []string { func (c *webClient) Permissions() []string {
return c.permissions return c.permissions
} }
......
...@@ -29,9 +29,6 @@ let serverConnection; ...@@ -29,9 +29,6 @@ let serverConnection;
/** @type {Object} */ /** @type {Object} */
let groupStatus = {}; let groupStatus = {};
/** @type {string} */
let username = null;
/** @type {string} */ /** @type {string} */
let token = null; let token = null;
...@@ -273,8 +270,11 @@ function setConnected(connected) { ...@@ -273,8 +270,11 @@ function setConnected(connected) {
} }
} }
/** @this {ServerConnection} */ /**
async function gotConnected() { * @this {ServerConnection}
* @param {string} [username]
*/
async function gotConnected(username) {
let credentials; let credentials;
if(token) { if(token) {
credentials = { credentials = {
...@@ -283,9 +283,9 @@ async function gotConnected() { ...@@ -283,9 +283,9 @@ async function gotConnected() {
}; };
token = null; token = null;
} else { } else {
username = getInputElement('username').value.trim();
setConnected(true); setConnected(true);
username = getInputElement('username').value.trim();
let pw = getInputElement('password').value; let pw = getInputElement('password').value;
getInputElement('password').value = ''; getInputElement('password').value = '';
if(!groupStatus.authServer) if(!groupStatus.authServer)
...@@ -2139,7 +2139,7 @@ function gotUser(id, kind) { ...@@ -2139,7 +2139,7 @@ function gotUser(id, kind) {
} }
function displayUsername() { function displayUsername() {
document.getElementById('userspan').textContent = username; document.getElementById('userspan').textContent = serverConnection.username;
let op = serverConnection.permissions.indexOf('op') >= 0; let op = serverConnection.permissions.indexOf('op') >= 0;
let present = serverConnection.permissions.indexOf('present') >= 0; let present = serverConnection.permissions.indexOf('present') >= 0;
let text = ''; let text = '';
...@@ -3776,7 +3776,6 @@ async function start() { ...@@ -3776,7 +3776,6 @@ async function start() {
setMediaChoices(false).then(e => reflectSettings()); setMediaChoices(false).then(e => reflectSettings());
if(parms.has('token')) { if(parms.has('token')) {
username = parms.get('username');
token = parms.get('token'); token = parms.get('token');
await serverConnect(); await serverConnect();
} else if(groupStatus.authPortal) { } else if(groupStatus.authPortal) {
......
...@@ -10,8 +10,6 @@ import ( ...@@ -10,8 +10,6 @@ import (
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
) )
var ErrUnexpectedSub = errors.New("unexpected 'sub' field")
func parseBase64(k string, d map[string]interface{}) ([]byte, error) { func parseBase64(k string, d map[string]interface{}) ([]byte, error) {
v, ok := d[k].(string) v, ok := d[k].(string)
if !ok { if !ok {
...@@ -117,18 +115,23 @@ func toStringArray(a []interface{}) ([]string, bool) { ...@@ -117,18 +115,23 @@ func toStringArray(a []interface{}) ([]string, bool) {
return b, true return b, true
} }
func Valid(username, token string, keys []map[string]interface{}) ([]string, []string, error) { func Valid(token string, keys []map[string]interface{}) (string, []string, []string, error) {
tok, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { tok, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
return getKey(t.Header, keys) return getKey(t.Header, keys)
}) })
if err != nil { if err != nil {
return nil, nil, err return "", nil, nil, err
} }
claims := tok.Claims.(jwt.MapClaims) claims := tok.Claims.(jwt.MapClaims)
sub, ok := claims["sub"].(string) var sub string
if !ok || sub != username { if s, ok := claims["sub"]; ok && s != nil {
return nil, nil, ErrUnexpectedSub ss, ok := s.(string)
if !ok {
return "", nil, nil,
errors.New("invalid 'sub' field")
}
sub = ss
} }
var aud []string var aud []string
...@@ -139,11 +142,11 @@ func Valid(username, token string, keys []map[string]interface{}) ([]string, []s ...@@ -139,11 +142,11 @@ func Valid(username, token string, keys []map[string]interface{}) ([]string, []s
case []interface{}: case []interface{}:
aud, ok = toStringArray(a) aud, ok = toStringArray(a)
if !ok { if !ok {
return nil, nil, return "", nil, nil,
errors.New("invalid 'aud' field") errors.New("invalid 'aud' field")
} }
default: default:
return nil, nil, return "", nil, nil,
errors.New("invalid 'aud' field") errors.New("invalid 'aud' field")
} }
} }
...@@ -152,14 +155,14 @@ func Valid(username, token string, keys []map[string]interface{}) ([]string, []s ...@@ -152,14 +155,14 @@ func Valid(username, token string, keys []map[string]interface{}) ([]string, []s
if p, ok := claims["permissions"]; ok && p != nil { if p, ok := claims["permissions"]; ok && p != nil {
pp, ok := p.([]interface{}) pp, ok := p.([]interface{})
if !ok { if !ok {
return nil, nil, return "", nil, nil,
errors.New("invalid 'permissions' field") errors.New("invalid 'permissions' field")
} }
perms, ok = toStringArray(pp) perms, ok = toStringArray(pp)
if !ok { if !ok {
return nil, nil, return "", nil, nil,
errors.New("invalid 'permissions' field") errors.New("invalid 'permissions' field")
} }
} }
return aud, perms, nil return sub, aud, perms, nil
} }
...@@ -69,11 +69,14 @@ func TestValid(t *testing.T) { ...@@ -69,11 +69,14 @@ func TestValid(t *testing.T) {
goodToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDI5NCwiZXhwIjoyOTA2NzUwMjk0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0.6xXpgBkBMn4PSBpnwYHb-gRn_Q97Yq9DoKkAf2_6iwc" goodToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDI5NCwiZXhwIjoyOTA2NzUwMjk0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0.6xXpgBkBMn4PSBpnwYHb-gRn_Q97Yq9DoKkAf2_6iwc"
aud, perms, err := Valid("john", goodToken, keys) sub, aud, perms, err := Valid(goodToken, keys)
if err != nil { if err != nil {
t.Errorf("Token invalid: %v", err) t.Errorf("Token invalid: %v", err)
} else { } else {
if sub != "john" {
t.Errorf("Unexpected sub: %v", sub)
}
if !reflect.DeepEqual(aud, []string{"https://galene.org:8443/group/auth/"}) { if !reflect.DeepEqual(aud, []string{"https://galene.org:8443/group/auth/"}) {
t.Errorf("Unexpected aud: %v", aud) t.Errorf("Unexpected aud: %v", aud)
} }
...@@ -82,14 +85,9 @@ func TestValid(t *testing.T) { ...@@ -82,14 +85,9 @@ func TestValid(t *testing.T) {
} }
} }
aud, perms, err = Valid("jack", goodToken, keys)
if err != ErrUnexpectedSub {
t.Errorf("Token should have bad username")
}
badToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDQ2OSwiZXhwIjoyOTA2NzUwNDY5LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0." badToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDQ2OSwiZXhwIjoyOTA2NzUwNDY5LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0."
_, _, err = Valid("john", badToken, keys) _, _, _, err = Valid(badToken, keys)
var verr *jwt.ValidationError var verr *jwt.ValidationError
if !errors.As(err, &verr) { if !errors.As(err, &verr) {
...@@ -98,14 +96,14 @@ func TestValid(t *testing.T) { ...@@ -98,14 +96,14 @@ func TestValid(t *testing.T) {
expiredToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDMyMiwiZXhwIjoxNjQ1MzEwMzUyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0.jyqRhoV6iK54SvlP33Fy630aDo-sLNmKKi1kcfqs378" expiredToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDMyMiwiZXhwIjoxNjQ1MzEwMzUyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0.jyqRhoV6iK54SvlP33Fy630aDo-sLNmKKi1kcfqs378"
_, _, err = Valid("john", expiredToken, keys) _, _, _, err = Valid(expiredToken, keys)
if !errors.As(err, &verr) { if !errors.As(err, &verr) {
t.Errorf("Token should be expired") t.Errorf("Token should be expired")
} }
noneToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDQwMSwiZXhwIjoxNjQ1MzEwNDMxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0." noneToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDQwMSwiZXhwIjoxNjQ1MzEwNDMxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0."
_, _, err = Valid("john", noneToken, keys) _, _, _, err = Valid(noneToken, keys)
if err == nil { if err == nil {
t.Errorf("Unsigned token should fail") t.Errorf("Unsigned token should fail")
} }
......
...@@ -586,7 +586,7 @@ func checkGroupPermissions(w http.ResponseWriter, r *http.Request, groupname str ...@@ -586,7 +586,7 @@ func checkGroupPermissions(w http.ResponseWriter, r *http.Request, groupname str
return false return false
} }
p, err := desc.GetPermission(groupname, _, p, err := desc.GetPermission(groupname,
group.ClientCredentials{ group.ClientCredentials{
Username: user, Username: user,
Password: pass, Password: pass,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment