From d501855450d06032311798ab65829c01199f69f1 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Tue, 7 Jun 2022 21:55:45 +0200 Subject: [PATCH] New IndieAuth version --- http.go | 2 +- httpRouters.go | 14 +++++--- indieAuthServer.go | 78 +++++++++++++++++++++++++++++------------ indieAuthServer_test.go | 4 ++- reactions.go | 2 +- ui.go | 1 + 6 files changed, 71 insertions(+), 30 deletions(-) diff --git a/http.go b/http.go index 7343606..e101cf6 100644 --- a/http.go +++ b/http.go @@ -168,7 +168,7 @@ func (a *goBlog) buildRouter() http.Handler { r.Route(micropubPath, a.micropubRouter) // IndieAuth - r.Route("/indieauth", a.indieAuthRouter) + r.Group(a.indieAuthRouter) // ActivityPub and stuff r.Group(a.activityPubRouter) diff --git a/httpRouters.go b/httpRouters.go index 87927ee..4ea8cce 100644 --- a/httpRouters.go +++ b/httpRouters.go @@ -22,11 +22,15 @@ func (a *goBlog) micropubRouter(r chi.Router) { // IndieAuth func (a *goBlog) indieAuthRouter(r chi.Router) { - r.Get("/", a.indieAuthRequest) - r.With(a.authMiddleware).Post("/accept", a.indieAuthAccept) - r.Post("/", a.indieAuthVerificationAuth) - r.Post("/token", a.indieAuthVerificationToken) - r.Get("/token", a.indieAuthTokenVerification) + r.Route(indieAuthPath, func(r chi.Router) { + r.Get("/", a.indieAuthRequest) + r.With(a.authMiddleware).Post("/accept", a.indieAuthAccept) + r.Post("/", a.indieAuthVerificationAuth) + r.Post(indieAuthTokenSubpath, a.indieAuthVerificationToken) + r.Get(indieAuthTokenSubpath, a.indieAuthTokenVerification) + r.Post(indieAuthTokenRevocationSubpath, a.indieAuthTokenRevokation) + }) + r.With(cacheLoggedIn, a.cacheMiddleware).Get("/.well-known/oauth-authorization-server", a.indieAuthMetadata) } // ActivityPub diff --git a/indieAuthServer.go b/indieAuthServer.go index aabd800..6a64da2 100644 --- a/indieAuthServer.go +++ b/indieAuthServer.go @@ -15,6 +15,14 @@ import ( "go.goblog.app/app/pkgs/contenttype" ) +// TODOs: +// - Expire tokens after a while +// - Userinfo endpoint + +const indieAuthPath = "/indieauth" +const indieAuthTokenSubpath = "/token" +const indieAuthTokenRevocationSubpath = "/revoke" + // https://www.w3.org/TR/indieauth/ // https://indieauth.spec.indieweb.org/ @@ -23,6 +31,29 @@ var ( errInvalidCode = errors.New("invalid code or code not found") ) +// Server Metadata +// https://indieauth.spec.indieweb.org/#x4-1-1-indieauth-server-metadata +func (a *goBlog) indieAuthMetadata(w http.ResponseWriter, r *http.Request) { + resp := map[string]any{ + "issuer": a.getFullAddress("/"), + "authorization_endpoint": a.getFullAddress(indieAuthPath), + "token_endpoint": a.getFullAddress(indieAuthPath + indieAuthTokenSubpath), + "introspection_endpoint": a.getFullAddress(indieAuthPath + indieAuthTokenSubpath), + "revocation_endpoint": a.getFullAddress(indieAuthPath + indieAuthTokenRevocationSubpath), + "revocation_endpoint_auth_methods_supported": []string{"none"}, + "scopes_supported": []string{"create", "update", "delete", "undelete", "media"}, + "code_challenge_methods_supported": indieauth.CodeChallengeMethods, + } + buf := bufferpool.Get() + defer bufferpool.Put(buf) + if err := json.NewEncoder(buf).Encode(resp); err != nil { + a.serveError(w, r, "Encoding failed", http.StatusInternalServerError) + return + } + w.Header().Set(contentType, contenttype.JSONUTF8) + _ = a.min.Get().Minify(contenttype.JSON, w, buf) +} + // Parse Authorization Request // https://indieauth.spec.indieweb.org/#authorization-request func (a *goBlog) indieAuthRequest(w http.ResponseWriter, r *http.Request) { @@ -56,17 +87,10 @@ func (a *goBlog) indieAuthAccept(w http.ResponseWriter, r *http.Request) { query := url.Values{} query.Set("code", code) query.Set("state", iareq.State) + query.Set("iss", a.getFullAddress("/")) http.Redirect(w, r, iareq.RedirectURI+"?"+query.Encode(), http.StatusFound) } -type tokenResponse struct { - Me string `json:"me,omitempty"` - ClientID string `json:"client_id,omitempty"` - Scope string `json:"scope,omitempty"` - Token string `json:"access_token,omitempty"` - TokenType string `json:"token_type,omitempty"` -} - // authorization endpoint // https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code // The client only exchanges the authorization code for the user's profile URL @@ -86,16 +110,22 @@ func (a *goBlog) indieAuthVerificationToken(w http.ResponseWriter, r *http.Reque a.serveError(w, r, err.Error(), http.StatusBadRequest) return } - // Token Revocation + // Token Revocation (old way) if r.Form.Get("action") == "revoke" { a.db.indieAuthRevokeToken(r.Form.Get("token")) - w.WriteHeader(http.StatusOK) return } // Token request a.indieAuthVerification(w, r, true) } +// Token Revocation (new way) +// https://indieauth.spec.indieweb.org/#token-revocation-p-4 +func (a *goBlog) indieAuthTokenRevokation(w http.ResponseWriter, r *http.Request) { + a.db.indieAuthRevokeToken(r.Form.Get("token")) + return +} + // Verify the authorization request with or without token response func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request, withToken bool) { // Get code and retrieve auth request @@ -123,8 +153,8 @@ func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request, w return } // Generate response - resp := &tokenResponse{ - Me: a.getFullAddress("") + "/", // MUST contain a path component / trailing slash + resp := map[string]any{ + "me": a.getFullAddress("") + "/", // MUST contain a path component / trailing slash } if withToken { // Generate and save token @@ -134,9 +164,9 @@ func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request, w return } // Add token to response - resp.TokenType = "Bearer" - resp.Token = token - resp.Scope = strings.Join(data.Scopes, " ") + resp["token_type"] = "Bearer" + resp["access_token"] = token + resp["scope"] = strings.Join(data.Scopes, " ") } buf := bufferpool.Get() defer bufferpool.Put(buf) @@ -190,17 +220,21 @@ func (db *database) indieAuthGetAuthRequest(code string) (data *indieauth.Authen // GET request to the token endpoint to check if the access token is valid func (a *goBlog) indieAuthTokenVerification(w http.ResponseWriter, r *http.Request) { data, err := a.db.indieAuthVerifyToken(r.Header.Get("Authorization")) + var res map[string]any if errors.Is(err, errInvalidToken) { - a.serveError(w, r, err.Error(), http.StatusUnauthorized) - return + res = map[string]any{ + "active": false, + } } else if err != nil { a.serveError(w, r, err.Error(), http.StatusInternalServerError) return - } - res := &tokenResponse{ - Scope: strings.Join(data.Scopes, " "), - Me: a.getFullAddress("") + "/", // MUST contain a path component / trailing slash - ClientID: data.ClientID, + } else { + res = map[string]any{ + "active": true, + "me": a.getFullAddress("") + "/", // MUST contain a path component / trailing slash + "client_id": data.ClientID, + "scope": strings.Join(data.Scopes, " "), + } } buf := bufferpool.Get() defer bufferpool.Put(buf) diff --git a/indieAuthServer_test.go b/indieAuthServer_test.go index fd21b21..3b744f5 100644 --- a/indieAuthServer_test.go +++ b/indieAuthServer_test.go @@ -145,6 +145,7 @@ func Test_indieAuthServer(t *testing.T) { req.Header.Set("Authorization", "Bearer "+token.AccessToken) app.d.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "\"active\":true") rec = httptest.NewRecorder() req = httptest.NewRequest(http.MethodPost, "https://example.org/indieauth/token?action=revoke&token="+token.AccessToken, nil) @@ -156,7 +157,8 @@ func Test_indieAuthServer(t *testing.T) { req = httptest.NewRequest(http.MethodGet, "https://example.org/indieauth/token", nil) req.Header.Set("Authorization", "Bearer "+token.AccessToken) app.d.ServeHTTP(rec, req) - assert.Equal(t, http.StatusUnauthorized, rec.Code) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "\"active\":false") } diff --git a/reactions.go b/reactions.go index 61b2b6c..a04fee4 100644 --- a/reactions.go +++ b/reactions.go @@ -108,7 +108,7 @@ func (a *goBlog) getReactionsFromDatabase(path string) (map[string]int, error) { return val.(map[string]int), nil } // Get reactions - res, err, _ := a.reactionsSfg.Do(path, func() (interface{}, error) { + res, err, _ := a.reactionsSfg.Do(path, func() (any, error) { // Build query sqlBuf := bufferpool.Get() defer bufferpool.Put(sqlBuf) diff --git a/ui.go b/ui.go index 5e27594..003ed7e 100644 --- a/ui.go +++ b/ui.go @@ -55,6 +55,7 @@ func (a *goBlog) renderBase(hb *htmlBuilder, rd *renderData, title, main func(hb // IndieAuth hb.writeElementOpen("link", "rel", "authorization_endpoint", "href", "/indieauth") hb.writeElementOpen("link", "rel", "token_endpoint", "href", "/indieauth/token") + hb.writeElementOpen("link", "rel", "indieauth-metadata", "href", "/.well-known/oauth-authorization-server") // Rel-Me user := a.cfg.User if user != nil {