mirror of https://github.com/jlelse/GoBlog
Use indieauth module
This commit is contained in:
parent
b16b8c9531
commit
e6e4f8f25d
3
app.go
3
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
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
alter table indieauthauth add challenge text not null default "";
|
||||
alter table indieauthauth add challengemethod text not null default "";
|
8
go.mod
8
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
|
||||
)
|
||||
|
|
12
go.sum
12
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=
|
||||
|
|
|
@ -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
|
||||
|
|
19
indieAuth.go
19
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, " "))))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
1
main.go
1
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")
|
||||
|
|
|
@ -40,6 +40,7 @@ const (
|
|||
templateGeoMap = "geomap"
|
||||
templateContact = "contact"
|
||||
templateEditorPreview = "editorpreview"
|
||||
templateIndieAuth = "indieauth"
|
||||
)
|
||||
|
||||
func (a *goBlog) initRendering() error {
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
<p><strong>redirect_uri:</strong> {{ .Data.RedirectURI }}</p>
|
||||
<input type="hidden" name="redirect_uri" value="{{ .Data.RedirectURI }}">
|
||||
<input type="hidden" name="state" value="{{ .Data.State }}">
|
||||
<input type="hidden" id="client_id" name="client_id" value="{{ .Data.ClientID }}">
|
||||
<input type="hidden" name="client_id" value="{{ .Data.ClientID }}">
|
||||
<input type="hidden" name="code_challenge" value="{{ .Data.CodeChallenge }}">
|
||||
<input type="hidden" name="code_challenge_method" value="{{ .Data.CodeChallengeMethod }}">
|
||||
<input type="submit" value="{{ string .Blog.Lang "authenticate" }}">
|
||||
</form>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue