diff --git a/userauth.go b/userauth.go index 2ad47aa..b6f9be1 100644 --- a/userauth.go +++ b/userauth.go @@ -7,9 +7,9 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "path/filepath" - "regexp" "strings" "github.com/golang-jwt/jwt/v4" @@ -73,15 +73,14 @@ func (u *ValidateFromToken) Authenticate(r *http.Request) (claims jwt.MapClaims, log.Debugf("Looking for key for %s", strIss) - re := regexp.MustCompile(`//([^/]*)`) - keyMatch := re.FindStringSubmatch(strIss) - if len(keyMatch) < 2 || keyMatch[1] == "" { - return nil, fmt.Errorf("failed to get issuer from token iss (%v)", strIss) + iss, err := url.ParseRequestURI(strIss) + if err != nil || iss.Hostname() == "" { + return nil, fmt.Errorf("failed to get issuer from token (%v)", strIss) } switch token.Header["alg"] { case "ES256": - key, err := jwt.ParseECPublicKeyFromPEM(u.pubkeys[keyMatch[1]]) + key, err := jwt.ParseECPublicKeyFromPEM(u.pubkeys[iss.Hostname()]) if err != nil { return nil, fmt.Errorf("failed to parse EC public key (%v)", err) } @@ -90,7 +89,7 @@ func (u *ValidateFromToken) Authenticate(r *http.Request) (claims jwt.MapClaims, return nil, fmt.Errorf("signed token (ES256) not valid: %v, (token was %s)", err, tokenStr) } case "RS256": - key, err := jwt.ParseRSAPublicKeyFromPEM(u.pubkeys[keyMatch[1]]) + key, err := jwt.ParseRSAPublicKeyFromPEM(u.pubkeys[iss.Hostname()]) if err != nil { return nil, fmt.Errorf("failed to parse RSA256 public key (%v)", err) } @@ -103,12 +102,17 @@ func (u *ValidateFromToken) Authenticate(r *http.Request) (claims jwt.MapClaims, } // Check whether token username and filepath match - re = regexp.MustCompile("/([^/]+)/") - username := re.FindStringSubmatch(r.URL.Path)[1] + str, err := url.ParseRequestURI(r.URL.Path) + if err != nil || str.Path == "" { + return nil, fmt.Errorf("failed to get path from query (%v)", r.URL.Path) + } + + path := strings.Split(str.Path, "/") + username := path[1] + // Case for Elixir and CEGA usernames: Replace @ with _ character if strings.Contains(fmt.Sprintf("%v", claims["sub"]), "@") { - claimString := fmt.Sprintf("%v", claims["sub"]) - if strings.ReplaceAll(claimString, "@", "_") != username { + if strings.ReplaceAll(fmt.Sprintf("%v", claims["sub"]), "@", "_") != username { return nil, fmt.Errorf("token supplied username %s but URL had %s", claims["sub"], username) } } else if claims["sub"] != username { @@ -120,7 +124,6 @@ func (u *ValidateFromToken) Authenticate(r *http.Request) (claims jwt.MapClaims, // Function for reading the ega key in []byte func (u *ValidateFromToken) getjwtkey(jwtpubkeypath string) error { - re := regexp.MustCompile(`(.*)\.+`) err := filepath.Walk(jwtpubkeypath, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -132,13 +135,8 @@ func (u *ValidateFromToken) getjwtkey(jwtpubkeypath string) error { if err != nil { return fmt.Errorf("token file error: %v", err) } - nameMatch := re.FindStringSubmatch(info.Name()) - - if nameMatch == nil || len(nameMatch) < 2 { - return fmt.Errorf("unexpected lack of substring match in filename %s", info.Name()) - } - - u.pubkeys[nameMatch[1]] = keyData + nameMatch := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name())) + u.pubkeys[nameMatch] = keyData } return nil @@ -152,18 +150,16 @@ func (u *ValidateFromToken) getjwtkey(jwtpubkeypath string) error { // Function for fetching the elixir key from the JWK and transform it to []byte func (u *ValidateFromToken) getjwtpubkey(jwtpubkeyurl string) error { - re := regexp.MustCompile("/([^/]+)/") - keyMatch := re.FindStringSubmatch(jwtpubkeyurl) - - if keyMatch == nil { - return fmt.Errorf("not valid link for key %s", jwtpubkeyurl) - } + jwkURL, err := url.ParseRequestURI(jwtpubkeyurl) + if err != nil || jwkURL.Scheme == "" || jwkURL.Host == "" { + if err != nil { + return err + } - if len(keyMatch) < 2 { - return fmt.Errorf("unexpected lack of submatches in %s", jwtpubkeyurl) + return fmt.Errorf("jwtpubkeyurl is not a proper URL (%s)", jwkURL) } + log.Debug("jwkURL: ", jwkURL.Scheme) - key := keyMatch[1] set, err := jwk.Fetch(jwtpubkeyurl) if err != nil { return fmt.Errorf("jwk.Fetch failed (%v) for %s", err, jwtpubkeyurl) @@ -198,8 +194,8 @@ func (u *ValidateFromToken) getjwtpubkey(jwtpubkeyurl string) error { Bytes: pkeyBytes, }, ) - u.pubkeys[key] = keyData - log.Debugf("Registered public key for %s", key) + u.pubkeys[jwkURL.Hostname()] = keyData + log.Debugf("Registered public key for %s", jwkURL.Hostname()) return nil } diff --git a/userauth_test.go b/userauth_test.go index f0030be..8c45237 100644 --- a/userauth_test.go +++ b/userauth_test.go @@ -25,9 +25,26 @@ func TestUserTokenAuthenticator_NoFile(t *testing.T) { a.pubkeys = make(map[string][]byte) err := a.getjwtkey("") assert.Error(t, err) +} - err = a.getjwtpubkey("") - assert.Error(t, err) +func TestUserTokenAuthenticator_GetFile(t *testing.T) { + // Create temp demo rsa key pair + demoKeysPath := "temp-rsa-keys" + prKeyPath, pubKeyPath, err := helper.MakeFolder(demoKeysPath) + assert.NoError(t, err) + + err = helper.CreateRSAkeys(prKeyPath, pubKeyPath) + assert.NoError(t, err) + + var pubkeys map[string][]byte + jwtpubkeypath := demoKeysPath + "/public-key/" + + a := NewValidateFromToken(pubkeys) + a.pubkeys = make(map[string][]byte) + err = a.getjwtkey(jwtpubkeypath) + assert.NoError(t, err) + + defer os.RemoveAll(demoKeysPath) } func TestUserTokenAuthenticator_WrongURL(t *testing.T) { @@ -37,7 +54,27 @@ func TestUserTokenAuthenticator_WrongURL(t *testing.T) { jwtpubkeyurl := "/dummy/" err := a.getjwtpubkey(jwtpubkeyurl) - assert.Error(t, err) + assert.Equal(t, "jwtpubkeyurl is not a proper URL (/dummy/)", err.Error()) +} + +func TestUserTokenAuthenticator_BadURL(t *testing.T) { + var pubkeys map[string][]byte + a := NewValidateFromToken(pubkeys) + a.pubkeys = make(map[string][]byte) + jwtpubkeyurl := "dummy.com/jwk" + + err := a.getjwtpubkey(jwtpubkeyurl) + assert.Equal(t, "parse \"dummy.com/jwk\": invalid URI for request", err.Error()) +} + +func TestUserTokenAuthenticator_GoodURL(t *testing.T) { + var pubkeys map[string][]byte + a := NewValidateFromToken(pubkeys) + a.pubkeys = make(map[string][]byte) + jwtpubkeyurl := "https://example.com/jwk/" + + err := a.getjwtpubkey(jwtpubkeyurl) + assert.ErrorContains(t, err, "failed to fetch remote JWK") } func TestUserTokenAuthenticator_ValidateSignature_RSA(t *testing.T) { @@ -251,6 +288,22 @@ func TestUserTokenAuthenticator_ValidateSignature_EC(t *testing.T) { _, err = a.Authenticate(r) assert.Contains(t, err.Error(), "failed to get issuer from token") + // Test LS-AAI user authentication + token, err := helper.CreateECToken(prKeyParsed, "ES256", "JWT", helper.WrongUserClaims) + assert.NoError(t, err) + + r, _ = http.NewRequest("", "/c5773f41d17d27bd53b1e6794aedc32d7906e779_elixir-europe.org/foo", nil) + r.Host = "localhost" + r.Header.Set("X-Amz-Security-Token", token) + _, err = a.Authenticate(r) + assert.NoError(t, err) + + r, _ = http.NewRequest("", "/dataset", nil) + r.Host = "localhost" + r.Header.Set("X-Amz-Security-Token", token) + _, err = a.Authenticate(r) + assert.Equal(t, "token supplied username c5773f41d17d27bd53b1e6794aedc32d7906e779@elixir-europe.org but URL had dataset", err.Error()) + defer os.RemoveAll(demoKeysPath) }