Commit c58064d9 authored by Juliusz Chroboczek's avatar Juliusz Chroboczek

Move token handling into the separate module.

Tokens are now an interface, and all the token logic is encapsulated
in the token module.
parent 59ff2531
......@@ -1111,81 +1111,66 @@ func readDescription(name string) (*Description, error) {
}
// called locked
func (g *Group) getPermission(creds ClientCredentials) (string, []string, error) {
func (g *Group) getPasswordPermission(creds ClientCredentials) ([]string, error) {
desc := g.description
if creds.Token == "" {
if !desc.AllowAnonymous && creds.Username == "" {
return "", nil, ErrAnonymousNotAuthorised
}
if found, good := matchClient(creds, desc.Op); found {
if good {
var p []string
p = []string{"op", "present"}
if desc.AllowRecording {
p = append(p, "record")
}
return creds.Username, p, nil
if !desc.AllowAnonymous && creds.Username == "" {
return nil, ErrAnonymousNotAuthorised
}
if found, good := matchClient(creds, desc.Op); found {
if good {
if desc.AllowRecording {
return []string{"op", "present", "record"}, nil
}
return "", nil, ErrNotAuthorised
return []string{"op", "present"}, nil
}
if found, good := matchClient(creds, desc.Presenter); found {
if good {
return creds.Username, []string{"present"}, nil
}
return "", nil, ErrNotAuthorised
return nil, ErrNotAuthorised
}
if found, good := matchClient(creds, desc.Presenter); found {
if good {
return []string{"present"}, nil
}
if found, good := matchClient(creds, desc.Other); found {
if good {
return creds.Username, nil, nil
}
return "", nil, ErrNotAuthorised
return nil, ErrNotAuthorised
}
if found, good := matchClient(creds, desc.Other); found {
if good {
return nil, nil
}
return "", nil, ErrNotAuthorised
return nil, ErrNotAuthorised
}
return nil, ErrNotAuthorised
}
sub, aud, perms, err := token.Valid(creds.Token, desc.AuthKeys)
if err != nil {
log.Printf("Token authentication: %v", err)
return "", nil, ErrNotAuthorised
}
if sub == nil {
log.Printf("Token authentication: token has no sub")
return "", nil, ErrNotAuthorised
}
username := *sub
if !desc.AllowAnonymous && username == "" {
return "", nil, ErrAnonymousNotAuthorised
}
conf, err := GetConfiguration()
if err != nil {
log.Printf("Read config.json: %v", err)
return "", nil, err
}
ok := false
for _, u := range aud {
url, err := url.Parse(u)
// called locked
func (g *Group) getPermission(creds ClientCredentials) (string, []string, error) {
desc := g.description
var username string
var perms []string
if creds.Token != "" {
tok, err := token.Parse(creds.Token, desc.AuthKeys)
if err != nil {
log.Printf("Token URL: %v", err)
continue
return "", nil, err
}
// 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
}
conf, err := GetConfiguration()
if err != nil {
return "", nil, err
}
if url.Path == path.Join("/group", g.name)+"/" {
ok = true
break
username, perms, err =
tok.Check(conf.CanonicalHost, g.name, &creds.Username)
if err != nil {
return "", nil, err
}
} else {
var err error
username = creds.Username
perms, err = g.getPasswordPermission(creds)
if err != nil {
return "", nil, err
}
}
if !ok {
return "", nil, ErrNotAuthorised
}
return username, perms, nil
}
......
package token
import (
"crypto/ecdsa"
"crypto/elliptic"
"encoding/base64"
"errors"
"math/big"
"net/url"
"path"
"strings"
"github.com/golang-jwt/jwt/v4"
)
type JWT jwt.Token
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 toStringArray(a []interface{}) ([]string, bool) {
b := make([]string, len(a))
for i, v := range a {
w, ok := v.(string)
if !ok {
return nil, false
}
b[i] = w
}
return b, true
}
func parseJWT(token string, keys []map[string]interface{}) (Token, error) {
t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
return getKey(t.Header, keys)
})
if err != nil {
return nil, err
}
return (*JWT)(t), nil
}
func (token *JWT) Check(host, group string, username *string) (string, []string, error) {
claims := token.Claims.(jwt.MapClaims)
s, ok := claims["sub"]
if !ok {
return "", nil, errors.New("token has no 'sub' field")
}
sub, ok := s.(string)
if !ok {
return "", nil, errors.New("invalid 'sub' field")
}
// we accept tokens with a different username from the one provided,
// and use the token's 'sub' field to override the username
var aud []string
if a, ok := claims["aud"]; ok && a != nil {
switch a := a.(type) {
case string:
aud = []string{a}
case []interface{}:
aud, ok = toStringArray(a)
if !ok {
return "", nil, errors.New("invalid 'aud' field")
}
default:
return "", nil, errors.New("invalid 'aud' field")
}
}
ok = false
for _, u := range aud {
url, err := url.Parse(u)
if err != nil {
continue
}
// if canonicalHost is not set, we allow tokens
// for any domain name. Hopefully different
// servers use distinct keys.
if host != "" {
if !strings.EqualFold(url.Host, host) {
continue
}
}
if url.Path == path.Join("/group", group)+"/" {
ok = true
break
}
}
if !ok {
return "", nil, errors.New("token for wrong group")
}
var perms []string
if p, ok := claims["permissions"]; ok && p != nil {
pp, ok := p.([]interface{})
if !ok {
return "", nil, errors.New("invalid 'permissions' field")
}
perms, ok = toStringArray(pp)
if !ok {
return "", nil, errors.New("invalid 'permissions' field")
}
}
return sub, perms, nil
}
......@@ -3,14 +3,11 @@ package token
import (
"crypto/ecdsa"
"encoding/json"
"errors"
"reflect"
"testing"
"github.com/golang-jwt/jwt/v4"
)
func TestHS256(t *testing.T) {
func TestJWKHS256(t *testing.T) {
key := `{
"kty":"oct",
"alg":"HS256",
......@@ -31,7 +28,7 @@ func TestHS256(t *testing.T) {
}
}
func TestES256(t *testing.T) {
func TestJWKES256(t *testing.T) {
key := `{
"kty":"EC",
"alg":"ES256",
......@@ -57,7 +54,7 @@ func TestES256(t *testing.T) {
}
}
func TestValid(t *testing.T) {
func TestJWT(t *testing.T) {
key := `{"alg":"HS256","k":"H7pCkktUl5KyPCZ7CKw09y1j460tfIv4dRcS1XstUKY","key_ops":["sign","verify"],"kty":"oct"}`
var k map[string]interface{}
err := json.Unmarshal([]byte(key), &k)
......@@ -66,76 +63,89 @@ func TestValid(t *testing.T) {
}
keys := []map[string]interface{}{k}
john := "john"
jack := "jack"
goodToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDI5NCwiZXhwIjoyOTA2NzUwMjk0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0.6xXpgBkBMn4PSBpnwYHb-gRn_Q97Yq9DoKkAf2_6iwc"
sub, aud, perms, err := Valid(goodToken, keys)
tok, err := Parse(goodToken, keys)
if err != nil {
t.Errorf("Couldn't parse goodToken: %v", err)
}
username, perms, err := tok.Check("galene.org:8443", "auth", &john)
if err != nil {
t.Errorf("goodToken is not valid: %v", err)
}
if username != "john" || !reflect.DeepEqual(perms, []string{"present"}) {
t.Errorf("Expected john, [present], got %v %v", username, perms)
}
username, perms, err = tok.Check("galene.org:8443", "auth", &jack)
if err != nil {
t.Errorf("goodToken is not valid: %v", err)
}
if username != "john" || !reflect.DeepEqual(perms, []string{"present"}) {
t.Errorf("Expected john, [present], got %v %v", username, perms)
}
username, perms, err = tok.Check("", "auth", &john)
if err != nil {
t.Errorf("goodToken is not valid: %v", err)
}
_, _, err = tok.Check("galene.org", "auth", &john)
if err == nil {
t.Errorf("goodToken is valid for wrong hostname")
}
_, _, err = tok.Check("galene.org:8443", "not-auth", &john)
if err == nil {
t.Errorf("goodToken is valid for wrong group")
}
emptySubToken := "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJhdWQiOiJodHRwczovL2dhbGVuZS5vcmc6ODQ0My9ncm91cC9hdXRoLyIsInBlcm1pc3Npb25zIjpbInByZXNlbnQiXSwiaWF0IjoxNjQ1MzEwMjk0LCJleHAiOjI5MDY3NTAyOTQsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MTIzNC8ifQo.xwpHIRzKAIgiHKG1pVQyZlXcolmvRwNvBm6FN2gTwZw"
tok, err = Parse(emptySubToken, keys)
if err != nil {
t.Errorf("Token invalid: %v", err)
} else {
if sub == nil || *sub != "john" {
t.Errorf("Unexpected sub: %v", sub)
}
if !reflect.DeepEqual(aud, []string{"https://galene.org:8443/group/auth/"}) {
t.Errorf("Unexpected aud: %v", aud)
}
if !reflect.DeepEqual(perms, []string{"present"}) {
t.Errorf("Unexpected perms: %v", perms)
}
}
anonymousToken := "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJhdWQiOiJodHRwczovL2dhbGVuZS5vcmc6ODQ0My9ncm91cC9hdXRoLyIsInBlcm1pc3Npb25zIjpbInByZXNlbnQiXSwiaWF0IjoxNjQ1MzEwMjk0LCJleHAiOjI5MDY3NTAyOTQsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MTIzNC8ifQo.xwpHIRzKAIgiHKG1pVQyZlXcolmvRwNvBm6FN2gTwZw"
sub, aud, perms, err = Valid(anonymousToken, keys)
t.Errorf("Couldn't parse emptySubToken: %v", err)
}
username, perms, err = tok.Check("galene.org:8443", "auth", &jack)
if err != nil {
t.Errorf("Token invalid: %v", err)
} else {
if sub == nil || *sub != "" {
t.Errorf("Unexpected sub: %v", sub)
}
if !reflect.DeepEqual(aud, []string{"https://galene.org:8443/group/auth/"}) {
t.Errorf("Unexpected aud: %v", aud)
}
if !reflect.DeepEqual(perms, []string{"present"}) {
t.Errorf("Unexpected perms: %v", perms)
}
t.Errorf("anonymousToken is not valid: %v", err)
}
if username != "" || !reflect.DeepEqual(perms, []string{"present"}) {
t.Errorf("Expected \"\", [present], got %v %v", username, perms)
}
noSubToken := "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJodHRwczovL2dhbGVuZS5vcmc6ODQ0My9ncm91cC9hdXRoLyIsInBlcm1pc3Npb25zIjpbInByZXNlbnQiXSwiaWF0IjoxNjQ1MzEwMjk0LCJleHAiOjI5MDY3NTAyOTQsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MTIzNC8ifQo.7LvoZEKPNVvsRe8SjLxmKa1TgjTA4ZQo2LMPJSXl-ro"
sub, aud, perms, err = Valid(noSubToken, keys)
tok, err = Parse(noSubToken, keys)
if err != nil {
t.Errorf("Token invalid: %v", err)
} else {
if sub != nil {
t.Errorf("Unexpected sub: %v", sub)
}
if !reflect.DeepEqual(aud, []string{"https://galene.org:8443/group/auth/"}) {
t.Errorf("Unexpected aud: %v", aud)
}
if !reflect.DeepEqual(perms, []string{"present"}) {
t.Errorf("Unexpected perms: %v", perms)
}
t.Errorf("Couldn't parse noSubToken: %v", err)
}
username, perms, err = tok.Check("galene.org:8443", "auth", &jack)
if err == nil {
t.Errorf("noSubToken is valid")
}
badToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDQ2OSwiZXhwIjoyOTA2NzUwNDY5LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0."
_, _, _, err = Valid(badToken, keys)
var verr *jwt.ValidationError
if !errors.As(err, &verr) {
t.Errorf("Token should fail")
_, err = Parse(badToken, keys)
if err == nil {
t.Errorf("badToken is good")
}
expiredToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDMyMiwiZXhwIjoxNjQ1MzEwMzUyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0.jyqRhoV6iK54SvlP33Fy630aDo-sLNmKKi1kcfqs378"
_, _, _, err = Valid(expiredToken, keys)
if !errors.As(err, &verr) {
t.Errorf("Token should be expired")
_, err = Parse(expiredToken, keys)
if err == nil {
t.Errorf("expiredToken is good")
}
noneToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzdWIiOiJqb2huIiwiYXVkIjoiaHR0cHM6Ly9nYWxlbmUub3JnOjg0NDMvZ3JvdXAvYXV0aC8iLCJwZXJtaXNzaW9ucyI6WyJwcmVzZW50Il0sImlhdCI6MTY0NTMxMDQwMSwiZXhwIjoxNjQ1MzEwNDMxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEyMzQvIn0."
_, _, _, err = Valid(noneToken, keys)
_, err = Parse(noneToken, keys)
if err == nil {
t.Errorf("Unsigned token should fail")
t.Errorf("noneToken is good")
}
}
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 toStringArray(a []interface{}) ([]string, bool) {
b := make([]string, len(a))
for i, v := range a {
w, ok := v.(string)
if !ok {
return nil, false
}
b[i] = w
}
return b, true
type Token interface {
Check(host, group string, username *string) (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) {
return getKey(t.Header, keys)
})
if err != nil {
return nil, nil, nil, err
}
claims := tok.Claims.(jwt.MapClaims)
var sub *string
if s, ok := claims["sub"]; ok && s != nil {
ss, ok := s.(string)
if !ok {
return nil, nil, nil,
errors.New("invalid 'sub' field")
}
sub = &ss
}
var aud []string
if a, ok := claims["aud"]; ok && a != nil {
switch a := a.(type) {
case string:
aud = []string{a}
case []interface{}:
aud, ok = toStringArray(a)
if !ok {
return nil, nil, nil,
errors.New("invalid 'aud' field")
}
default:
return nil, nil, nil,
errors.New("invalid 'aud' field")
}
}
var perms []string
if p, ok := claims["permissions"]; ok && p != nil {
pp, ok := p.([]interface{})
if !ok {
return nil, nil, nil,
errors.New("invalid 'permissions' field")
}
perms, ok = toStringArray(pp)
if !ok {
return nil, nil, nil,
errors.New("invalid 'permissions' field")
}
}
return sub, aud, perms, nil
func Parse(token string, keys []map[string]interface{}) (Token, error) {
return parseJWT(token, keys)
}
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