Use indieauth module

This commit is contained in:
Jan-Lukas Else 2021-11-23 15:23:01 +01:00
parent b16b8c9531
commit e6e4f8f25d
10 changed files with 201 additions and 205 deletions

3
app.go
View File

@ -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

2
dbmigrations/00025.sql Normal file
View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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

View File

@ -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, " "))))
})
}

View File

@ -1,153 +1,194 @@
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"`
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)
}
valid, err := a.db.verifyAuthorization(data)
// 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
}
// 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
}
// Check grant type
if grantType := r.Form.Get("grant_type"); grantType != "" && grantType != "authorization_code" {
a.serveError(w, r, "unknown grant type", http.StatusBadRequest)
return
}
// 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
}
if !valid {
a.serveError(w, r, "Authentication not valid", http.StatusForbidden)
return
// Add token to response
resp.TokenType = "Bearer"
resp.Token = token
resp.Scope = strings.Join(data.Scopes, " ")
}
b, _ := json.Marshal(tokenResponse{
Me: a.getFullAddress("") + "/", // MUST contain a path component / trailing slash
})
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"))
// 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
}
// 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 {
a.serveError(w, r, "Invalid token or token not found", http.StatusUnauthorized)
return nil, err
}
data = &indieauth.AuthenticationRequest{}
var scope string
err = row.Scan(&data.ClientID, &data.RedirectURI, &scope, &data.CodeChallenge, &data.CodeChallengeMethod)
if err == sql.ErrNoRows {
return nil, errInvalidCode
} else if err != nil {
return nil, err
}
if scope != "" {
data.Scopes = strings.Split(scope, " ")
}
// Delete the auth code and expired auth codes
_, _ = db.exec("delete from indieauthauth where code = ? or time < ?", code, maxAge)
return data, nil
}
// 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{
@ -155,126 +196,43 @@ func (a *goBlog) indieAuthToken(w http.ResponseWriter, r *http.Request) {
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
}
}
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)
if err != nil {
return false, err
}
scope := ""
err = row.Scan(&data.code, &data.ClientID, &data.RedirectURI, &scope)
if err == sql.ErrNoRows {
return false, nil
} else if err != nil {
return false, 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
}
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
}
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)
}

View File

@ -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")

View File

@ -40,6 +40,7 @@ const (
templateGeoMap = "geomap"
templateContact = "contact"
templateEditorPreview = "editorpreview"
templateIndieAuth = "indieauth"
)
func (a *goBlog) initRendering() error {

View File

@ -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>