Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
galene
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexedi
galene
Commits
03811db3
Commit
03811db3
authored
Oct 29, 2021
by
Juliusz Chroboczek
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement token authentication.
parent
b4d1ef39
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
433 additions
and
28 deletions
+433
-28
README
README
+72
-16
README.PROTOCOL
README.PROTOCOL
+42
-0
go.mod
go.mod
+2
-1
go.sum
go.sum
+2
-0
group/client.go
group/client.go
+1
-0
group/group.go
group/group.go
+58
-0
rtpconn/webclient.go
rtpconn/webclient.go
+2
-0
static/galene.js
static/galene.js
+16
-4
static/protocol.js
static/protocol.js
+37
-3
token/token.go
token/token.go
+138
-0
token/token_test.go
token/token_test.go
+54
-0
webserver/webserver.go
webserver/webserver.go
+9
-4
No files found.
README
View file @
03811db3
...
@@ -91,9 +91,10 @@ optional, but unless you specify at least one user definition (`op`,
...
@@ -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:
...
@@ -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 user
name
*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 password
s
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:
...
@@ -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>.
...
...
README.PROTOCOL
View file @
03811db3
...
@@ -39,6 +39,11 @@ configuration file. Each object has the following fields:
...
@@ -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:
...
@@ -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.
...
@@ -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).
go.mod
View file @
03811db3
module github.com/jech/galene
module github.com/jech/galene
go 1.1
3
go 1.1
5
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
...
...
go.sum
View file @
03811db3
...
@@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
...
@@ -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=
...
...
group/client.go
View file @
03811db3
...
@@ -92,6 +92,7 @@ type ClientCredentials struct {
...
@@ -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
{
...
...
group/group.go
View file @
03811db3
...
@@ -4,6 +4,7 @@ import (
...
@@ -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 (
...
@@ -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 {
...
@@ -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
...
@@ -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
...
@@ -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 {
...
@@ -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 {
...
@@ -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
,
}
}
...
...
rtpconn/webclient.go
View file @
03811db3
...
@@ -113,6 +113,7 @@ type clientMessage struct {
...
@@ -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 {
...
@@ -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
{
...
...
static/galene.js
View file @
03811db3
...
@@ -271,13 +271,25 @@ function setConnected(connected) {
...
@@ -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
);
...
...
static/protocol.js
View file @
03811db3
...
@@ -205,6 +205,7 @@ function ServerConnection() {
...
@@ -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) {
...
@@ -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
);
};
};
...
...
token/token.go
0 → 100644
View file @
03811db3
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
}
token/token_test.go
0 → 100644
View file @
03811db3
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"
)
}
}
webserver/webserver.go
View file @
03811db3
...
@@ -86,9 +86,13 @@ func Serve(address string, dataDir string) error {
...
@@ -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) {
...
@@ -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) {
...
@@ -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"
))
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment