From c58064d92397030ccd1e54a23f00f722c5ec7edc Mon Sep 17 00:00:00 2001 From: Juliusz Chroboczek Date: Wed, 22 Mar 2023 00:07:13 +0100 Subject: [PATCH] Move token handling into the separate module. Tokens are now an interface, and all the token logic is encapsulated in the token module. --- group/group.go | 119 +++++++--------- token/jwt.go | 197 +++++++++++++++++++++++++++ token/{token_test.go => jwt_test.go} | 116 +++++++++------- token/token.go | 167 +---------------------- 4 files changed, 316 insertions(+), 283 deletions(-) create mode 100644 token/jwt.go rename token/{token_test.go => jwt_test.go} (56%) diff --git a/group/group.go b/group/group.go index 0737bad..6ce7438 100644 --- a/group/group.go +++ b/group/group.go @@ -1110,82 +1110,67 @@ func readDescription(name string) (*Description, error) { return &desc, nil } +// called locked +func (g *Group) getPasswordPermission(creds ClientCredentials) ([]string, error) { + desc := g.description + + 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 []string{"op", "present"}, nil + } + return nil, ErrNotAuthorised + } + if found, good := matchClient(creds, desc.Presenter); found { + if good { + return []string{"present"}, nil + } + return nil, ErrNotAuthorised + } + if found, good := matchClient(creds, desc.Other); found { + if good { + return nil, nil + } + return nil, ErrNotAuthorised + } + return nil, ErrNotAuthorised +} + // called locked func (g *Group) getPermission(creds ClientCredentials) (string, []string, error) { desc := g.description - if creds.Token == "" { - if !desc.AllowAnonymous && creds.Username == "" { - return "", nil, ErrAnonymousNotAuthorised + var username string + var perms []string + if creds.Token != "" { + tok, err := token.Parse(creds.Token, desc.AuthKeys) + if err != nil { + return "", nil, err } - 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 - } - return "", nil, ErrNotAuthorised + + conf, err := GetConfiguration() + if err != nil { + return "", nil, err } - if found, good := matchClient(creds, desc.Presenter); found { - if good { - return creds.Username, []string{"present"}, nil - } - return "", nil, ErrNotAuthorised + + username, perms, err = + tok.Check(conf.CanonicalHost, g.name, &creds.Username) + if err != nil { + return "", nil, err } - if found, good := matchClient(creds, desc.Other); found { - if good { - return creds.Username, nil, nil - } - return "", nil, ErrNotAuthorised + } else { + var err error + username = creds.Username + perms, err = g.getPasswordPermission(creds) + if err != nil { + return "", nil, err } - 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) - 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", g.name)+"/" { - ok = true - break - } - } - if !ok { - return "", nil, ErrNotAuthorised - } return username, perms, nil } diff --git a/token/jwt.go b/token/jwt.go new file mode 100644 index 0000000..a009dd0 --- /dev/null +++ b/token/jwt.go @@ -0,0 +1,197 @@ +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 +} diff --git a/token/token_test.go b/token/jwt_test.go similarity index 56% rename from token/token_test.go rename to token/jwt_test.go index c6d7dee..bb385f8 100644 --- a/token/token_test.go +++ b/token/jwt_test.go @@ -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("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) - } + t.Errorf("Couldn't parse goodToken: %v", err) } - anonymousToken := "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJhdWQiOiJodHRwczovL2dhbGVuZS5vcmc6ODQ0My9ncm91cC9hdXRoLyIsInBlcm1pc3Npb25zIjpbInByZXNlbnQiXSwiaWF0IjoxNjQ1MzEwMjk0LCJleHAiOjI5MDY3NTAyOTQsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MTIzNC8ifQo.xwpHIRzKAIgiHKG1pVQyZlXcolmvRwNvBm6FN2gTwZw" - - sub, aud, perms, err = Valid(anonymousToken, keys) + username, perms, err := tok.Check("galene.org:8443", "auth", &john) 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("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("Couldn't parse emptySubToken: %v", err) + } + username, perms, err = tok.Check("galene.org:8443", "auth", &jack) + if err != nil { + 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") } } diff --git a/token/token.go b/token/token.go index 043f074..512df1b 100644 --- a/token/token.go +++ b/token/token.go @@ -1,168 +1,9 @@ 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 +type Token interface { + Check(host, group string, username *string) (string, []string, error) } -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 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) }