mirror of https://github.com/jlelse/GoBlog
New IndieAuth version
This commit is contained in:
parent
9225370967
commit
d501855450
2
http.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)
|
||||
|
|
|
@ -22,11 +22,15 @@ func (a *goBlog) micropubRouter(r chi.Router) {
|
|||
|
||||
// IndieAuth
|
||||
func (a *goBlog) indieAuthRouter(r chi.Router) {
|
||||
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("/token", a.indieAuthVerificationToken)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
} 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, " "),
|
||||
}
|
||||
res := &tokenResponse{
|
||||
Scope: strings.Join(data.Scopes, " "),
|
||||
Me: a.getFullAddress("") + "/", // MUST contain a path component / trailing slash
|
||||
ClientID: data.ClientID,
|
||||
}
|
||||
buf := bufferpool.Get()
|
||||
defer bufferpool.Put(buf)
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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
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…
Reference in New Issue