New IndieAuth version

This commit is contained in:
Jan-Lukas Else 2022-06-07 21:55:45 +02:00
parent 9225370967
commit d501855450
6 changed files with 71 additions and 30 deletions

View File

@ -168,7 +168,7 @@ func (a *goBlog) buildRouter() http.Handler {
r.Route(micropubPath, a.micropubRouter) r.Route(micropubPath, a.micropubRouter)
// IndieAuth // IndieAuth
r.Route("/indieauth", a.indieAuthRouter) r.Group(a.indieAuthRouter)
// ActivityPub and stuff // ActivityPub and stuff
r.Group(a.activityPubRouter) r.Group(a.activityPubRouter)

View File

@ -22,11 +22,15 @@ func (a *goBlog) micropubRouter(r chi.Router) {
// IndieAuth // IndieAuth
func (a *goBlog) indieAuthRouter(r chi.Router) { func (a *goBlog) indieAuthRouter(r chi.Router) {
r.Get("/", a.indieAuthRequest) r.Route(indieAuthPath, func(r chi.Router) {
r.With(a.authMiddleware).Post("/accept", a.indieAuthAccept) r.Get("/", a.indieAuthRequest)
r.Post("/", a.indieAuthVerificationAuth) r.With(a.authMiddleware).Post("/accept", a.indieAuthAccept)
r.Post("/token", a.indieAuthVerificationToken) r.Post("/", a.indieAuthVerificationAuth)
r.Get("/token", a.indieAuthTokenVerification) 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 // ActivityPub

View File

@ -15,6 +15,14 @@ import (
"go.goblog.app/app/pkgs/contenttype" "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://www.w3.org/TR/indieauth/
// https://indieauth.spec.indieweb.org/ // https://indieauth.spec.indieweb.org/
@ -23,6 +31,29 @@ var (
errInvalidCode = errors.New("invalid code or code not found") 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 // Parse Authorization Request
// https://indieauth.spec.indieweb.org/#authorization-request // https://indieauth.spec.indieweb.org/#authorization-request
func (a *goBlog) indieAuthRequest(w http.ResponseWriter, r *http.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 := url.Values{}
query.Set("code", code) query.Set("code", code)
query.Set("state", iareq.State) query.Set("state", iareq.State)
query.Set("iss", a.getFullAddress("/"))
http.Redirect(w, r, iareq.RedirectURI+"?"+query.Encode(), http.StatusFound) 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 // authorization endpoint
// https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code // https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code
// The client only exchanges the authorization code for the user's profile URL // 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) a.serveError(w, r, err.Error(), http.StatusBadRequest)
return return
} }
// Token Revocation // Token Revocation (old way)
if r.Form.Get("action") == "revoke" { if r.Form.Get("action") == "revoke" {
a.db.indieAuthRevokeToken(r.Form.Get("token")) a.db.indieAuthRevokeToken(r.Form.Get("token"))
w.WriteHeader(http.StatusOK)
return return
} }
// Token request // Token request
a.indieAuthVerification(w, r, true) 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 // Verify the authorization request with or without token response
func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request, withToken bool) { func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request, withToken bool) {
// Get code and retrieve auth request // Get code and retrieve auth request
@ -123,8 +153,8 @@ func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request, w
return return
} }
// Generate response // Generate response
resp := &tokenResponse{ resp := map[string]any{
Me: a.getFullAddress("") + "/", // MUST contain a path component / trailing slash "me": a.getFullAddress("") + "/", // MUST contain a path component / trailing slash
} }
if withToken { if withToken {
// Generate and save token // Generate and save token
@ -134,9 +164,9 @@ func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request, w
return return
} }
// Add token to response // Add token to response
resp.TokenType = "Bearer" resp["token_type"] = "Bearer"
resp.Token = token resp["access_token"] = token
resp.Scope = strings.Join(data.Scopes, " ") resp["scope"] = strings.Join(data.Scopes, " ")
} }
buf := bufferpool.Get() buf := bufferpool.Get()
defer bufferpool.Put(buf) 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 // GET request to the token endpoint to check if the access token is valid
func (a *goBlog) indieAuthTokenVerification(w http.ResponseWriter, r *http.Request) { func (a *goBlog) indieAuthTokenVerification(w http.ResponseWriter, r *http.Request) {
data, err := a.db.indieAuthVerifyToken(r.Header.Get("Authorization")) data, err := a.db.indieAuthVerifyToken(r.Header.Get("Authorization"))
var res map[string]any
if errors.Is(err, errInvalidToken) { if errors.Is(err, errInvalidToken) {
a.serveError(w, r, err.Error(), http.StatusUnauthorized) res = map[string]any{
return "active": false,
}
} else if err != nil { } else if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError) a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return return
} } else {
res := &tokenResponse{ res = map[string]any{
Scope: strings.Join(data.Scopes, " "), "active": true,
Me: a.getFullAddress("") + "/", // MUST contain a path component / trailing slash "me": a.getFullAddress("") + "/", // MUST contain a path component / trailing slash
ClientID: data.ClientID, "client_id": data.ClientID,
"scope": strings.Join(data.Scopes, " "),
}
} }
buf := bufferpool.Get() buf := bufferpool.Get()
defer bufferpool.Put(buf) defer bufferpool.Put(buf)

View File

@ -145,6 +145,7 @@ func Test_indieAuthServer(t *testing.T) {
req.Header.Set("Authorization", "Bearer "+token.AccessToken) req.Header.Set("Authorization", "Bearer "+token.AccessToken)
app.d.ServeHTTP(rec, req) app.d.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "\"active\":true")
rec = httptest.NewRecorder() rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "https://example.org/indieauth/token?action=revoke&token="+token.AccessToken, nil) 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 = httptest.NewRequest(http.MethodGet, "https://example.org/indieauth/token", nil)
req.Header.Set("Authorization", "Bearer "+token.AccessToken) req.Header.Set("Authorization", "Bearer "+token.AccessToken)
app.d.ServeHTTP(rec, req) 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")
} }

View File

@ -108,7 +108,7 @@ func (a *goBlog) getReactionsFromDatabase(path string) (map[string]int, error) {
return val.(map[string]int), nil return val.(map[string]int), nil
} }
// Get reactions // Get reactions
res, err, _ := a.reactionsSfg.Do(path, func() (interface{}, error) { res, err, _ := a.reactionsSfg.Do(path, func() (any, error) {
// Build query // Build query
sqlBuf := bufferpool.Get() sqlBuf := bufferpool.Get()
defer bufferpool.Put(sqlBuf) defer bufferpool.Put(sqlBuf)

1
ui.go
View File

@ -55,6 +55,7 @@ func (a *goBlog) renderBase(hb *htmlBuilder, rd *renderData, title, main func(hb
// IndieAuth // IndieAuth
hb.writeElementOpen("link", "rel", "authorization_endpoint", "href", "/indieauth") hb.writeElementOpen("link", "rel", "authorization_endpoint", "href", "/indieauth")
hb.writeElementOpen("link", "rel", "token_endpoint", "href", "/indieauth/token") hb.writeElementOpen("link", "rel", "token_endpoint", "href", "/indieauth/token")
hb.writeElementOpen("link", "rel", "indieauth-metadata", "href", "/.well-known/oauth-authorization-server")
// Rel-Me // Rel-Me
user := a.cfg.User user := a.cfg.User
if user != nil { if user != nil {