diff --git a/README.API b/README.API index 90826e5..d0d379f 100644 --- a/README.API +++ b/README.API @@ -55,6 +55,14 @@ on-disk format but without any user definitions or cryptographic keys. Allowed methods are HEAD, GET, PUT and DELETE. The only accepted content-type is `application/json`. +### Authentication keys + + /galene-api/0/.groups/groupname/.keys + +Contains the keys used for validation of stateless tokens, encoded as +a JSON key set (RFC 7517). Allowed methods are PUT and DELETE. The only +accepted content-type is `application/jwk-set+json`. + ### List of users /galene-api/0/.groups/groupname/.users/ diff --git a/group/description.go b/group/description.go index e83a8fc..37c43de 100644 --- a/group/description.go +++ b/group/description.go @@ -540,6 +540,18 @@ func GetDescriptionNames() ([]string, error) { return names, err } +func SetKeys(group string, keys []map[string]any) error { + groups.mu.Lock() + defer groups.mu.Unlock() + + desc, err := readDescription(group, false) + if err != nil { + return err + } + desc.AuthKeys = keys + return rewriteDescriptionFile(desc.FileName, desc) +} + func GetUsers(group string) ([]string, string, error) { desc, err := GetDescription(group) if err != nil { diff --git a/webserver/api.go b/webserver/api.go index 439ead9..855a805 100644 --- a/webserver/api.go +++ b/webserver/api.go @@ -128,6 +128,9 @@ func apiGroupHandler(w http.ResponseWriter, r *http.Request, pth string) { if kind == ".users" { apiUserHandler(w, r, g, rest) return + } else if kind == ".keys" && rest == "" { + keysHandler(w, r, g) + return } else if kind != "" { if !checkAdmin(w, r) { return @@ -257,7 +260,7 @@ func apiUserHandler(w http.ResponseWriter, r *http.Request, g, pth string) { } first2, kind2, rest2 := splitPath(pth) - if kind2 == ".password" && first2 != "" && rest2 == "" { + if first2 != "" && kind2 == ".password" && rest2 == "" { passwordHandler(w, r, g, first2[1:]) return } else if kind2 != "" || first2 == "" { @@ -429,3 +432,47 @@ func passwordHandler(w http.ResponseWriter, r *http.Request, g, user string) { methodNotAllowed(w, "PUT", "POST", "DELETE") return } + +type jwkset = struct { + Keys []map[string]any `json:"keys"` +} + +func keysHandler(w http.ResponseWriter, r *http.Request, g string) { + if !checkAdmin(w, r) { + return + } + + if r.Method == "PUT" { + ctype := parseContentType(r.Header.Get("Content-Type")) + if !strings.EqualFold(ctype, "application/jwk-set+json") { + http.Error(w, "unsupported content type", + http.StatusUnsupportedMediaType) + return + } + d := json.NewDecoder(r.Body) + var keys jwkset + err := d.Decode(&keys) + if err != nil { + httpError(w, err) + return + } + err = group.SetKeys(g, keys.Keys) + if err != nil { + httpError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) + return + } else if r.Method == "DELETE" { + err := group.SetKeys(g, nil) + if err != nil { + httpError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) + return + } + + methodNotAllowed(w, "PUT", "DELETE") + return +} diff --git a/webserver/api_test.go b/webserver/api_test.go index 5a7073f..dad34b7 100644 --- a/webserver/api_test.go +++ b/webserver/api_test.go @@ -168,6 +168,16 @@ func TestApi(t *testing.T) { t.Errorf("Get groups: %v %#v", err, s) } + resp, err = do("PUT", "/galene-api/0/.groups/test/.keys", + "application/jwk-set+json", "", "", + `{"keys": [{ + "kty": "oct", "alg": "HS256", + "k": "4S9YZLHK1traIaXQooCnPfBw_yR8j9VEPaAMWAog_YQ" + }]}`) + if err != nil || resp.StatusCode != http.StatusNoContent { + t.Errorf("Set key: %v %v", err, resp.StatusCode) + } + s, err = getString("/galene-api/0/.groups/test/.users/") if err != nil || s != "" { t.Errorf("Get users: %v", err) @@ -251,6 +261,16 @@ func TestApi(t *testing.T) { t.Errorf("Users (after delete): %#v", desc.Users) } + if len(desc.AuthKeys) != 1 { + t.Errorf("Keys: %v", len(desc.AuthKeys)) + } + + resp, err = do("DELETE", "/galene-api/0/.groups/test/.keys", + "", "", "", "") + if err != nil || resp.StatusCode != http.StatusNoContent { + t.Errorf("Delete keys: %v %v", err, resp.StatusCode) + } + resp, err = do("DELETE", "/galene-api/0/.groups/test/", "", "", "", "") if err != nil || resp.StatusCode != http.StatusNoContent {