From e6e4f8f25d8c6c4ed02643651a1612ff6e799430 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Tue, 23 Nov 2021 15:23:01 +0100 Subject: [PATCH] Use indieauth module --- app.go | 3 + dbmigrations/00025.sql | 2 + go.mod | 8 +- go.sum | 12 +- httpRouters.go | 6 +- indieAuth.go | 19 +- indieAuthServer.go | 350 ++++++++++++++++--------------------- main.go | 1 + render.go | 1 + templates/indieauth.gohtml | 4 +- 10 files changed, 201 insertions(+), 205 deletions(-) create mode 100644 dbmigrations/00025.sql diff --git a/app.go b/app.go index e4c58a8..9ce8d9f 100644 --- a/app.go +++ b/app.go @@ -10,6 +10,7 @@ import ( ts "git.jlel.se/jlelse/template-strings" ct "github.com/elnormous/contenttype" "github.com/go-fed/httpsig" + "github.com/hacdias/indieauth" rotatelogs "github.com/lestrrat-go/file-rotatelogs" "github.com/yuin/goldmark" "go.goblog.app/app/pkgs/minify" @@ -50,6 +51,8 @@ type goBlog struct { httpClient httpClient // HTTP Routers d http.Handler + // IndieAuth + ias *indieauth.Server // Logs logf *rotatelogs.RotateLogs // Markdown diff --git a/dbmigrations/00025.sql b/dbmigrations/00025.sql new file mode 100644 index 0000000..91a5e2f --- /dev/null +++ b/dbmigrations/00025.sql @@ -0,0 +1,2 @@ +alter table indieauthauth add challenge text not null default ""; +alter table indieauthauth add challengemethod text not null default ""; \ No newline at end of file diff --git a/go.mod b/go.mod index 8bf0ce6..47784dd 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.2.1 github.com/gorilla/websocket v1.4.2 + github.com/hacdias/indieauth v1.5.0 github.com/jlaffaye/ftp v0.0.0-20211117213618-11820403398b // master github.com/jlelse/feeds v1.2.1-0.20210704161900-189f94254ad4 @@ -57,7 +58,7 @@ require ( golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b nhooyr.io/websocket v1.8.7 - tailscale.com v1.18.0 + tailscale.com v1.18.1 // main willnorris.com/go/microformats v1.1.2-0.20210827044458-ff2a6ae41971 ) @@ -81,6 +82,7 @@ require ( github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1 // indirect github.com/godbus/dbus/v5 v5.0.5 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/gorilla/css v1.0.0 // indirect @@ -116,13 +118,17 @@ require ( go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 // indirect golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45 // indirect golang.zx2c4.com/wireguard/windows v0.4.10 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.27.1 // indirect gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 // indirect inet.af/netstack v0.0.0-20211101182044-1c1bcf452982 // indirect + willnorris.com/go/webmention v0.0.0-20211028201829-b0044f1a24d0 // indirect ) diff --git a/go.sum b/go.sum index 969de06..976c1a0 100644 --- a/go.sum +++ b/go.sum @@ -268,6 +268,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hacdias/indieauth v1.5.0 h1:BugufHJs4G3HCHCImTfwziUYhjQ5IssKpGcJ4SFPvfY= +github.com/hacdias/indieauth v1.5.0/go.mod h1:e/YWhcIgtz/WR/ZledToHTE3Xx0VBu18y84rhzLpzWY= github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -503,6 +505,7 @@ github.com/vishvananda/netlink v1.1.1-0.20211101163509-b10eb8fe5cf6 h1:167a2omrz github.com/vishvananda/netlink v1.1.1-0.20211101163509-b10eb8fe5cf6/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -631,6 +634,7 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0= golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -648,6 +652,8 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -986,7 +992,9 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237 h1:iAEkCBPbRaflBgZ7o9gjVUuWuvWeV4sytFWg9o+Pj2k= -tailscale.com v1.18.0 h1:riytbiTTxaglL9an6VrD03xWvPOPN8vD3VOFOvdB3Io= -tailscale.com v1.18.0/go.mod h1:XzG4o2vtYFkVvmJWPaTGSaOzqlKSRx2WU+aJbrxaVE0= +tailscale.com v1.18.1 h1:3hkMsdpREdz2w0O3YcmOgJkl95ChTT4Dje7wq8prD/E= +tailscale.com v1.18.1/go.mod h1:XzG4o2vtYFkVvmJWPaTGSaOzqlKSRx2WU+aJbrxaVE0= willnorris.com/go/microformats v1.1.2-0.20210827044458-ff2a6ae41971 h1:b4juh5znIpBA1KnzHMP0UB4Cs+3/0b0XfchkWE81FXw= willnorris.com/go/microformats v1.1.2-0.20210827044458-ff2a6ae41971/go.mod h1:kvVnWrkkEscVAIITCEoiTX66Hcyg59C7q0E49mb9TJ0= +willnorris.com/go/webmention v0.0.0-20211028201829-b0044f1a24d0 h1:3/ozQ2qGZat82ON3AYMTot3gCg/vU7tgn/LYSJbkVPM= +willnorris.com/go/webmention v0.0.0-20211028201829-b0044f1a24d0/go.mod h1:DgeruqKIsZtcDXVXNbBHa0YYEm88oAnK7PahkDtuCvw= diff --git a/httpRouters.go b/httpRouters.go index 48d1379..1589699 100644 --- a/httpRouters.go +++ b/httpRouters.go @@ -24,9 +24,9 @@ func (a *goBlog) micropubRouter(r chi.Router) { func (a *goBlog) indieAuthRouter(r chi.Router) { r.Get("/", a.indieAuthRequest) r.With(a.authMiddleware).Post("/accept", a.indieAuthAccept) - r.Post("/", a.indieAuthVerification) - r.Get("/token", a.indieAuthToken) - r.Post("/token", a.indieAuthToken) + r.Post("/", a.indieAuthVerificationAuth) + r.Post("/token", a.indieAuthVerificationToken) + r.Get("/token", a.indieAuthTokenVerification) } // ActivityPub diff --git a/indieAuth.go b/indieAuth.go index b2e9961..2dc4688 100644 --- a/indieAuth.go +++ b/indieAuth.go @@ -4,22 +4,37 @@ import ( "context" "net/http" "strings" + "time" + + "github.com/hacdias/indieauth" ) const indieAuthScope contextKey = "scope" +func (a *goBlog) initIndieAuth() { + a.ias = indieauth.NewServer( + false, + &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DisableKeepAlives: true, + }, + }, + ) +} + func (a *goBlog) checkIndieAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { bearerToken := r.Header.Get("Authorization") if len(bearerToken) == 0 { bearerToken = r.URL.Query().Get("access_token") } - tokenData, err := a.db.verifyIndieAuthToken(bearerToken) + data, err := a.db.indieAuthVerifyToken(bearerToken) if err != nil { a.serveError(w, r, err.Error(), http.StatusUnauthorized) return } - next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), indieAuthScope, strings.Join(tokenData.Scopes, " ")))) + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), indieAuthScope, strings.Join(data.Scopes, " ")))) }) } diff --git a/indieAuthServer.go b/indieAuthServer.go index 33fc5c0..7c6d586 100644 --- a/indieAuthServer.go +++ b/indieAuthServer.go @@ -1,280 +1,238 @@ package main import ( - "crypto/sha1" "database/sql" "encoding/json" "errors" - "fmt" "net/http" "net/url" "strings" "time" - "github.com/spf13/cast" + "github.com/google/uuid" + "github.com/hacdias/indieauth" "go.goblog.app/app/pkgs/contenttype" ) // https://www.w3.org/TR/indieauth/ // https://indieauth.spec.indieweb.org/ -type indieAuthData struct { - ClientID string - RedirectURI string - State string - Scopes []string - code string - token string - time time.Time -} +var ( + errInvalidToken = errors.New("invalid token or token not found") + errInvalidCode = errors.New("invalid code or code not found") +) +// Parse Authorization Request +// https://indieauth.spec.indieweb.org/#authorization-request func (a *goBlog) indieAuthRequest(w http.ResponseWriter, r *http.Request) { - // Authorization request - if err := r.ParseForm(); err != nil { + iareq, err := a.ias.ParseAuthorization(r) + if err != nil { a.serveError(w, r, err.Error(), http.StatusBadRequest) return } - data := &indieAuthData{ - ClientID: r.Form.Get("client_id"), - RedirectURI: r.Form.Get("redirect_uri"), - State: r.Form.Get("state"), - } - if rt := r.Form.Get("response_type"); rt != "code" && rt != "id" && rt != "" { - a.serveError(w, r, "response_type must be code", http.StatusBadRequest) - return - } - if scope := r.Form.Get("scope"); scope != "" { - data.Scopes = strings.Split(scope, " ") - } - if !isValidProfileURL(data.ClientID) || !isValidProfileURL(data.RedirectURI) { - a.serveError(w, r, "client_id and redirect_uri need to by valid URLs", http.StatusBadRequest) - return - } - if data.State == "" { - a.serveError(w, r, "state must not be empty", http.StatusBadRequest) - return - } - a.render(w, r, "indieauth", &renderData{ - Data: data, + // Render page that let's the user authorize the app + a.render(w, r, templateIndieAuth, &renderData{ + Data: iareq, }) } -func isValidProfileURL(profileURL string) bool { - u, err := url.Parse(profileURL) - if err != nil { - return false - } - if u.Scheme != "http" && u.Scheme != "https" { - return false - } - if u.Fragment != "" { - return false - } - if u.User.String() != "" { - return false - } - if u.Port() != "" { - return false - } - // Missing: Check domain / ip - return true -} - +// The user accepted the authorization request +// Authorization response +// https://indieauth.spec.indieweb.org/#authorization-response func (a *goBlog) indieAuthAccept(w http.ResponseWriter, r *http.Request) { - // Authentication flow - if err := r.ParseForm(); err != nil { + iareq, err := a.ias.ParseAuthorization(r) + if err != nil { a.serveError(w, r, err.Error(), http.StatusBadRequest) return } - data := &indieAuthData{ - ClientID: r.Form.Get("client_id"), - RedirectURI: r.Form.Get("redirect_uri"), - State: r.Form.Get("state"), - Scopes: r.Form["scopes"], - time: time.Now().UTC(), - } - sha := sha1.New() - if _, err := sha.Write([]byte(data.time.Format(time.RFC3339) + data.ClientID)); err != nil { - a.serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - data.code = fmt.Sprintf("%x", sha.Sum(nil)) - err := a.db.saveAuthorization(data) + // Save the authorization request + code, err := a.db.indieAuthSaveAuthRequest(iareq) if err != nil { a.serveError(w, r, err.Error(), http.StatusInternalServerError) return } - http.Redirect(w, r, data.RedirectURI+"?code="+data.code+"&state="+data.State, http.StatusFound) + // Build a redirect + query := url.Values{} + query.Set("code", code) + query.Set("state", iareq.State) + http.Redirect(w, r, iareq.RedirectURI+"?"+query.Encode(), http.StatusFound) } type tokenResponse struct { - AccessToken string `json:"access_token,omitempty"` - TokenType string `json:"token_type,omitempty"` - Scope string `json:"scope,omitempty"` - Me string `json:"me,omitempty"` - ClientID string `json:"client_id,omitempty"` + 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"` } -func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request) { - // Authorization verification +// authorization endpoint +// https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code +// The client only exchanges the authorization code for the user's profile URL +func (a *goBlog) indieAuthVerificationAuth(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { a.serveError(w, r, err.Error(), http.StatusBadRequest) return } - data := &indieAuthData{ - code: r.Form.Get("code"), - ClientID: r.Form.Get("client_id"), - RedirectURI: r.Form.Get("redirect_uri"), + a.indieAuthVerification(w, r, false) +} + +// token endpoint +// https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code +// The client exchanges the authorization code for an access token and the user's profile URL +func (a *goBlog) indieAuthVerificationToken(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + a.serveError(w, r, err.Error(), http.StatusBadRequest) + return } - valid, err := a.db.verifyAuthorization(data) - if err != nil { + // Token Revocation + 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) +} + +// 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 + code := r.Form.Get("code") + if code == "" { + a.serveError(w, r, "missing code parameter", http.StatusBadRequest) + return + } + data, err := a.db.indieAuthGetAuthRequest(code) + if errors.Is(err, errInvalidCode) { + a.serveError(w, r, err.Error(), http.StatusBadRequest) + return + } else if err != nil { a.serveError(w, r, err.Error(), http.StatusInternalServerError) return } - if !valid { - a.serveError(w, r, "Authentication not valid", http.StatusForbidden) + // Check grant type + if grantType := r.Form.Get("grant_type"); grantType != "" && grantType != "authorization_code" { + a.serveError(w, r, "unknown grant type", http.StatusBadRequest) return } - b, _ := json.Marshal(tokenResponse{ + // Validate token exchange + if err = a.ias.ValidateTokenExchange(data, r); err != nil { + a.serveError(w, r, err.Error(), http.StatusBadRequest) + return + } + // Generate response + resp := &tokenResponse{ Me: a.getFullAddress("") + "/", // MUST contain a path component / trailing slash - }) + } + if withToken { + // Generate and save token + token, err := a.db.indieAuthSaveToken(data) + if err != nil { + a.serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + // Add token to response + resp.TokenType = "Bearer" + resp.Token = token + resp.Scope = strings.Join(data.Scopes, " ") + } + b, _ := json.Marshal(resp) w.Header().Set(contentType, contenttype.JSONUTF8) _, _ = a.min.Write(w, contenttype.JSON, b) } -func (a *goBlog) indieAuthToken(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - // Token verification - data, err := a.db.verifyIndieAuthToken(r.Header.Get("Authorization")) - if err != nil { - a.serveError(w, r, "Invalid token or token not found", http.StatusUnauthorized) - return - } - res := &tokenResponse{ - Scope: strings.Join(data.Scopes, " "), - Me: a.getFullAddress("") + "/", // MUST contain a path component / trailing slash - ClientID: data.ClientID, - } - b, _ := json.Marshal(res) - w.Header().Set(contentType, contenttype.JSONUTF8) - _, _ = a.min.Write(w, contenttype.JSON, b) - return - } else if r.Method == http.MethodPost { - if err := r.ParseForm(); err != nil { - a.serveError(w, r, err.Error(), http.StatusBadRequest) - return - } - // Token Revocation - if r.Form.Get("action") == "revoke" { - a.db.revokeIndieAuthToken(r.Form.Get("token")) - w.WriteHeader(http.StatusOK) - return - } - // Token request - if r.Form.Get("grant_type") == "authorization_code" { - data := &indieAuthData{ - code: r.Form.Get("code"), - ClientID: r.Form.Get("client_id"), - RedirectURI: r.Form.Get("redirect_uri"), - } - valid, err := a.db.verifyAuthorization(data) - if err != nil { - a.serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - if !valid { - a.serveError(w, r, "Authentication not valid", http.StatusForbidden) - return - } - if len(data.Scopes) < 1 { - a.serveError(w, r, "No scope", http.StatusBadRequest) - return - } - data.time = time.Now().UTC() - sha := sha1.New() - if _, err := sha.Write([]byte(data.time.Format(time.RFC3339) + data.ClientID)); err != nil { - a.serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - data.token = fmt.Sprintf("%x", sha.Sum(nil)) - err = a.db.saveToken(data) - if err != nil { - a.serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - res := &tokenResponse{ - TokenType: "Bearer", - AccessToken: data.token, - Scope: strings.Join(data.Scopes, " "), - Me: a.getFullAddress("") + "/", // MUST contain a path component / trailing slash - } - b, _ := json.Marshal(res) - w.Header().Set(contentType, contenttype.JSONUTF8) - _, _ = a.min.Write(w, contenttype.JSON, b) - return - } - a.serveError(w, r, "", http.StatusBadRequest) - return - } +// Save the authorization request and return the code +func (db *database) indieAuthSaveAuthRequest(data *indieauth.AuthenticationRequest) (string, error) { + // Generate a code to identify the request + code := uuid.NewString() + // Save the request + _, err := db.exec( + "insert into indieauthauth (time, code, client, redirect, scope, challenge, challengemethod) values (?, ?, ?, ?, ?, ?, ?)", + time.Now().UTC().Unix(), code, data.ClientID, data.RedirectURI, strings.Join(data.Scopes, " "), data.CodeChallenge, data.CodeChallengeMethod, + ) + return code, err } -func (db *database) saveAuthorization(data *indieAuthData) (err error) { - _, err = db.exec("insert into indieauthauth (time, code, client, redirect, scope) values (?, ?, ?, ?, ?)", data.time.Unix(), data.code, data.ClientID, data.RedirectURI, strings.Join(data.Scopes, " ")) - return -} - -func (db *database) verifyAuthorization(data *indieAuthData) (valid bool, err error) { - // code valid for 600 seconds - row, err := db.queryRow("select code, client, redirect, scope from indieauthauth where time >= ? and code = ? and client = ? and redirect = ?", time.Now().Unix()-600, data.code, data.ClientID, data.RedirectURI) +// Retrieve the auth request from the database to continue the authorization process +func (db *database) indieAuthGetAuthRequest(code string) (data *indieauth.AuthenticationRequest, err error) { + // code valid for 10 minutes + maxAge := time.Now().UTC().Add(-10 * time.Minute).Unix() + // Query the database + row, err := db.queryRow("select client, redirect, scope, challenge, challengemethod from indieauthauth where time >= ? and code = ?", maxAge, code) if err != nil { - return false, err + return nil, err } - scope := "" - err = row.Scan(&data.code, &data.ClientID, &data.RedirectURI, &scope) + data = &indieauth.AuthenticationRequest{} + var scope string + err = row.Scan(&data.ClientID, &data.RedirectURI, &scope, &data.CodeChallenge, &data.CodeChallengeMethod) if err == sql.ErrNoRows { - return false, nil + return nil, errInvalidCode } else if err != nil { - return false, err + return nil, err } if scope != "" { data.Scopes = strings.Split(scope, " ") } - valid = true - _, err = db.exec("delete from indieauthauth where code = ? or time < ?", data.code, time.Now().Unix()-600) - data.code = "" - return + // Delete the auth code and expired auth codes + _, _ = db.exec("delete from indieauthauth where code = ? or time < ?", code, maxAge) + return data, nil } -func (db *database) saveToken(data *indieAuthData) (err error) { - _, err = db.exec("insert into indieauthtoken (time, token, client, scope) values (?, ?, ?, ?)", data.time.Unix(), data.token, data.ClientID, strings.Join(data.Scopes, " ")) - return +// Access token verification request (https://indieauth.spec.indieweb.org/#access-token-verification-request) +// +// 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")) + if errors.Is(err, errInvalidToken) { + a.serveError(w, r, err.Error(), http.StatusUnauthorized) + return + } 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, + } + w.Header().Set(contentType, contenttype.JSONUTF8) + b, _ := json.Marshal(res) + _, _ = a.min.Write(w, contenttype.JSON, b) } -func (db *database) verifyIndieAuthToken(token string) (data *indieAuthData, err error) { +// Checks the database for the token and returns the indieAuthData with client and scope. +// +// Returns errInvalidToken if the token is invalid. +func (db *database) indieAuthVerifyToken(token string) (data *indieauth.AuthenticationRequest, err error) { token = strings.ReplaceAll(token, "Bearer ", "") - data = &indieAuthData{ - Scopes: []string{}, - } - row, err := db.queryRow("select time, token, client, scope from indieauthtoken where token = @token", sql.Named("token", token)) + data = &indieauth.AuthenticationRequest{Scopes: []string{}} + row, err := db.queryRow("select client, scope from indieauthtoken where token = @token", sql.Named("token", token)) if err != nil { return nil, err } - timeString := "" - scope := "" - err = row.Scan(&timeString, &data.token, &data.ClientID, &scope) + var scope string + err = row.Scan(&data.ClientID, &scope) if err == sql.ErrNoRows { - return nil, errors.New("token not found") + return nil, errInvalidToken } else if err != nil { return nil, err } if scope != "" { data.Scopes = strings.Split(scope, " ") } - data.time = time.Unix(cast.ToInt64(timeString), 0) return } -func (db *database) revokeIndieAuthToken(token string) { +// Save a new token to the database +func (db *database) indieAuthSaveToken(data *indieauth.AuthenticationRequest) (string, error) { + token := uuid.NewString() + _, err := db.exec("insert into indieauthtoken (time, token, client, scope) values (?, ?, ?, ?)", time.Now().UTC().Unix(), token, data.ClientID, strings.Join(data.Scopes, " ")) + return token, err +} + +// Revoke and delete the token from the database +func (db *database) indieAuthRevokeToken(token string) { if token != "" { _, _ = db.exec("delete from indieauthtoken where token=?", token) } diff --git a/main.go b/main.go index a570e60..14b6abe 100644 --- a/main.go +++ b/main.go @@ -174,6 +174,7 @@ func (app *goBlog) initComponents(logging bool) { app.initTelegram() app.initBlogStats() app.initSessions() + app.initIndieAuth() // Log finish if logging { log.Println("Initialized components") diff --git a/render.go b/render.go index 2a6d443..d4f60e2 100644 --- a/render.go +++ b/render.go @@ -40,6 +40,7 @@ const ( templateGeoMap = "geomap" templateContact = "contact" templateEditorPreview = "editorpreview" + templateIndieAuth = "indieauth" ) func (a *goBlog) initRendering() error { diff --git a/templates/indieauth.gohtml b/templates/indieauth.gohtml index 83d02f1..c372dc0 100644 --- a/templates/indieauth.gohtml +++ b/templates/indieauth.gohtml @@ -19,7 +19,9 @@

redirect_uri: {{ .Data.RedirectURI }}

- + + +