Browse Source

New IndieAuth version

master
Jan-Lukas Else 3 weeks ago
parent
commit
d501855450
  1. 2
      http.go
  2. 14
      httpRouters.go
  3. 78
      indieAuthServer.go
  4. 4
      indieAuthServer_test.go
  5. 2
      reactions.go
  6. 1
      ui.go

2
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)

14
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

78
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)

4
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")
}

2
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)

1
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 {

Loading…
Cancel
Save