Update IndieAuth (w/o PKCE for now)

This commit is contained in:
Jan-Lukas Else 2020-12-09 17:25:09 +01:00
parent 0be61598ef
commit db1c4901c7
8 changed files with 108 additions and 125 deletions

View File

@ -56,7 +56,10 @@ func prepareAppDbStatement(query string) (*sql.Stmt, error) {
dbStatementCacheMutex.Unlock()
return stmt, nil
})
return stmt.(*sql.Stmt), err
if err != nil {
return nil, err
}
return stmt.(*sql.Stmt), nil
}
func appDbExec(query string, args ...interface{}) (sql.Result, error) {

View File

@ -71,6 +71,22 @@ func migrateDb() error {
return err
},
},
&migrator.Migration{
Name: "00006",
Func: func(tx *sql.Tx) error {
_, err := tx.Exec(`
create table indieauthauthnew (time text not null, code text not null, client text not null, redirect text not null, scope text not null);
insert into indieauthauthnew (time, code, client, redirect, scope) select time, code, client, redirect, scope from indieauthauth;
drop table indieauthauth;
alter table indieauthauthnew rename to indieauthauth;
create table indieauthtokennew (time text not null, token text not null, client text not null, scope text not null);
insert into indieauthtokennew (time, token, client, scope) select time, token, client, scope from indieauthtoken;
drop table indieauthtoken;
alter table indieauthtokennew rename to indieauthtoken;
`)
return err
},
},
),
)
if err != nil {

6
go.mod
View File

@ -39,7 +39,7 @@ require (
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/smartystreets/assertions v1.2.0 // indirect
github.com/snabb/sitemap v1.0.0
github.com/spf13/afero v1.5.0 // indirect
github.com/spf13/afero v1.5.1 // indirect
github.com/spf13/cast v1.3.1
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.7.1
@ -57,12 +57,12 @@ require (
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 // indirect
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
golang.org/x/mod v0.4.0 // indirect
golang.org/x/net v0.0.0-20201207224615-747e23833adb // indirect
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 // indirect
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d // indirect
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b // indirect
golang.org/x/text v0.3.4 // indirect
golang.org/x/tools v0.0.0-20201208062317-e652b2f42cc7 // indirect
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

13
go.sum
View File

@ -287,8 +287,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.5.0 h1:8Wb647pxgVlypPIdcDlffCLCHCElBZ1sCF6i85qNvRw=
github.com/spf13/afero v1.5.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg=
github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
@ -418,8 +418,8 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201207224615-747e23833adb h1:xj2oMIbduz83x7tzglytWT7spn6rP+9hvKjTpro6/pM=
golang.org/x/net v0.0.0-20201207224615-747e23833adb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 h1:lwlPPsmjDKK0J6eG6xDWd5XPehI0R024zxjDnw3esPA=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -460,6 +460,7 @@ golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d h1:MiWWjyhUzZ+jvhZvloX6ZrUsd
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q=
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -496,8 +497,8 @@ golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnf
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201208062317-e652b2f42cc7 h1:2OSu5vYyX4LVqZAtqZXnFEcN26SDKIJYlEVIRl1tj8U=
golang.org/x/tools v0.0.0-20201208062317-e652b2f42cc7/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2 h1:vEtypaVub6UvKkiXZ2xx9QIvp9TL7sI7xp7vdi2kezA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=

View File

@ -109,9 +109,9 @@ func buildHandler() (http.Handler, error) {
// IndieAuth
r.Route("/indieauth", func(indieauthRouter chi.Router) {
indieauthRouter.Use(middleware.NoCache)
indieauthRouter.With(authMiddleware, minifier.Middleware).Get("/", indieAuthAuthGet)
indieauthRouter.With(minifier.Middleware).Get("/", indieAuthRequest)
indieauthRouter.With(authMiddleware).Post("/accept", indieAuthAccept)
indieauthRouter.Post("/", indieAuthAuthPost)
indieauthRouter.Post("/", indieAuthVerification)
indieauthRouter.Get("/token", indieAuthToken)
indieauthRouter.Post("/token", indieAuthToken)
})

View File

@ -3,7 +3,6 @@ package main
import (
"context"
"net/http"
"net/http/httptest"
"strings"
)
@ -18,12 +17,7 @@ func checkIndieAuth(next http.Handler) http.Handler {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if !isAllowedHost(httptest.NewRequest(http.MethodGet, tokenData.Me, nil), appConfig.Server.Domain) {
http.Error(w, "Forbidden", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "scope", strings.Join(tokenData.Scopes, " "))
next.ServeHTTP(w, r.WithContext(ctx))
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "scope", strings.Join(tokenData.Scopes, " "))))
return
})
}

View File

@ -14,85 +14,46 @@ import (
)
// https://www.w3.org/TR/indieauth/
// https://indieauth.spec.indieweb.org/
type indieAuthData struct {
Me string
ClientID string
RedirectURI string
State string
ResponseType string
Scopes []string
code string
token string
time time.Time
ClientID string
RedirectURI string
State string
Scopes []string
code string
token string
time time.Time
}
func indieAuthAuthGet(w http.ResponseWriter, r *http.Request) {
// Authentication / authorization request
func indieAuthRequest(w http.ResponseWriter, r *http.Request) {
// Authorization request
r.ParseForm()
data := &indieAuthData{
Me: r.Form.Get("me"),
ClientID: r.Form.Get("client_id"),
RedirectURI: r.Form.Get("redirect_uri"),
State: r.Form.Get("state"),
ResponseType: r.Form.Get("response_type"),
ClientID: r.Form.Get("client_id"),
RedirectURI: r.Form.Get("redirect_uri"),
State: r.Form.Get("state"),
}
if data.ResponseType == "" {
data.ResponseType = "id"
if rt := r.Form.Get("response_type"); rt != "code" && rt != "id" && rt != "" {
http.Error(w, "response_type must be code", http.StatusBadRequest)
return
}
if scope := r.Form.Get("scope"); scope != "" {
data.Scopes = strings.Split(scope, " ")
}
if !isValidProfileURL(data.Me) || !isValidProfileURL(data.ClientID) || !isValidProfileURL(data.RedirectURI) {
http.Error(w, "me, client_id and redirect_uri need to by valid URLs", http.StatusBadRequest)
if !isValidProfileURL(data.ClientID) || !isValidProfileURL(data.RedirectURI) {
http.Error(w, "client_id and redirect_uri need to by valid URLs", http.StatusBadRequest)
return
}
if data.State == "" {
http.Error(w, "state must not be empty", http.StatusBadRequest)
return
}
if data.ResponseType != "id" && data.ResponseType != "code" {
http.Error(w, "response_type must be empty or id or code", http.StatusBadRequest)
return
}
// if data.ResponseType == "code" && len(data.Scopes) < 1 {
// http.Error(w, "scope is missing or empty", http.StatusBadRequest)
// return
// }
render(w, "indieauth", &renderData{
Data: data,
})
}
func indieAuthAuthPost(w http.ResponseWriter, r *http.Request) {
// Authentication verification
r.ParseForm()
data := &indieAuthData{
code: r.Form.Get("code"),
ClientID: r.Form.Get("client_id"),
RedirectURI: r.Form.Get("redirect_uri"),
}
valid, err := data.verifyAuthorization(true)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !valid {
http.Error(w, "Authentication not valid", http.StatusForbidden)
return
}
res := &tokenResponse{
Me: data.Me,
}
w.Header().Add(contentType, contentTypeJSONUTF8)
err = json.NewEncoder(w).Encode(res)
if err != nil {
w.Header().Del(contentType)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func isValidProfileURL(profileURL string) bool {
u, err := url.Parse(profileURL)
if err != nil {
@ -101,8 +62,6 @@ func isValidProfileURL(profileURL string) bool {
if u.Scheme != "http" && u.Scheme != "https" {
return false
}
// Missing: Check path
// Missing: Check single/double dot path
if u.Fragment != "" {
return false
}
@ -120,16 +79,14 @@ func indieAuthAccept(w http.ResponseWriter, r *http.Request) {
// Authentication flow
r.ParseForm()
data := &indieAuthData{
Me: r.Form.Get("me"),
ClientID: r.Form.Get("client_id"),
RedirectURI: r.Form.Get("redirect_uri"),
State: r.Form.Get("state"),
ResponseType: r.Form.Get("response_type"),
Scopes: r.Form["scopes"],
time: time.Now(),
ClientID: r.Form.Get("client_id"),
RedirectURI: r.Form.Get("redirect_uri"),
State: r.Form.Get("state"),
Scopes: r.Form["scopes"],
time: time.Now(),
}
sha := sha1.New()
sha.Write([]byte(data.time.String() + data.Me + data.ClientID))
sha.Write([]byte(data.time.String() + data.ClientID))
data.code = fmt.Sprintf("%x", sha.Sum(nil))
err := data.saveAuthorization()
if err != nil {
@ -147,6 +104,35 @@ type tokenResponse struct {
ClientID string `json:"client_id,omitempty"`
}
func indieAuthVerification(w http.ResponseWriter, r *http.Request) {
// Authorization verification
r.ParseForm()
data := &indieAuthData{
code: r.Form.Get("code"),
ClientID: r.Form.Get("client_id"),
RedirectURI: r.Form.Get("redirect_uri"),
}
valid, err := data.verifyAuthorization()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !valid {
http.Error(w, "Authentication not valid", http.StatusForbidden)
return
}
res := &tokenResponse{
Me: appConfig.Server.PublicAddress,
}
w.Header().Add(contentType, contentTypeJSONUTF8)
err = json.NewEncoder(w).Encode(res)
if err != nil {
w.Header().Del(contentType)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func indieAuthToken(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
// Token verification
@ -157,7 +143,7 @@ func indieAuthToken(w http.ResponseWriter, r *http.Request) {
}
res := &tokenResponse{
Scope: strings.Join(data.Scopes, " "),
Me: data.Me,
Me: appConfig.Server.PublicAddress,
ClientID: data.ClientID,
}
w.Header().Add(contentType, contentTypeJSONUTF8)
@ -182,9 +168,8 @@ func indieAuthToken(w http.ResponseWriter, r *http.Request) {
code: r.Form.Get("code"),
ClientID: r.Form.Get("client_id"),
RedirectURI: r.Form.Get("redirect_uri"),
Me: r.Form.Get("me"),
}
valid, err := data.verifyAuthorization(false)
valid, err := data.verifyAuthorization()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -199,7 +184,7 @@ func indieAuthToken(w http.ResponseWriter, r *http.Request) {
}
data.time = time.Now()
sha := sha1.New()
sha.Write([]byte(data.time.String() + data.Me + data.ClientID))
sha.Write([]byte(data.time.String() + data.ClientID))
data.token = fmt.Sprintf("%x", sha.Sum(nil))
err = data.saveToken()
if err != nil {
@ -210,7 +195,7 @@ func indieAuthToken(w http.ResponseWriter, r *http.Request) {
TokenType: "Bearer",
AccessToken: data.token,
Scope: strings.Join(data.Scopes, " "),
Me: data.Me,
Me: appConfig.Server.PublicAddress,
}
w.Header().Add(contentType, contentTypeJSONUTF8)
err = json.NewEncoder(w).Encode(res)
@ -227,38 +212,25 @@ func indieAuthToken(w http.ResponseWriter, r *http.Request) {
}
func (data *indieAuthData) saveAuthorization() (err error) {
_, err = appDbExec("insert into indieauthauth (time, code, me, client, redirect, scope) values (?, ?, ?, ?, ?, ?)", data.time.Unix(), data.code, data.Me, data.ClientID, data.RedirectURI, strings.Join(data.Scopes, " "))
_, err = appDbExec("insert into indieauthauth (time, code, client, redirect, scope) values (?, ?, ?, ?, ?)", data.time.Unix(), data.code, data.ClientID, data.RedirectURI, strings.Join(data.Scopes, " "))
return
}
func (data *indieAuthData) verifyAuthorization(authentication bool) (valid bool, err error) {
func (data *indieAuthData) verifyAuthorization() (valid bool, err error) {
// code valid for 600 seconds
if !authentication {
row, err := appDbQueryRow("select code, me, client, redirect, scope from indieauthauth where time >= ? and code = ? and me = ? and client = ? and redirect = ?", time.Now().Unix()-600, data.code, data.Me, data.ClientID, data.RedirectURI)
if err != nil {
return false, err
}
scope := ""
err = row.Scan(&data.code, &data.Me, &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, " ")
}
} else {
row, err := appDbQueryRow("select code, me, client, redirect 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
}
err = row.Scan(&data.code, &data.Me, &data.ClientID, &data.RedirectURI)
if err == sql.ErrNoRows {
return false, nil
} else if err != nil {
return false, err
}
row, err := appDbQueryRow("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 = appDbExec("delete from indieauthauth where code = ? or time < ?", data.code, time.Now().Unix()-600)
@ -267,7 +239,7 @@ func (data *indieAuthData) verifyAuthorization(authentication bool) (valid bool,
}
func (data *indieAuthData) saveToken() (err error) {
_, err = appDbExec("insert into indieauthtoken (time, token, me, client, scope) values (?, ?, ?, ?, ?)", data.time.Unix(), data.token, data.Me, data.ClientID, strings.Join(data.Scopes, " "))
_, err = appDbExec("insert into indieauthtoken (time, token, client, scope) values (?, ?, ?, ?)", data.time.Unix(), data.token, data.ClientID, strings.Join(data.Scopes, " "))
return
}
@ -276,13 +248,13 @@ func verifyIndieAuthToken(token string) (data *indieAuthData, err error) {
data = &indieAuthData{
Scopes: []string{},
}
row, err := appDbQueryRow("select time, token, me, client, scope from indieauthtoken where token = @token", sql.Named("token", token))
row, err := appDbQueryRow("select time, token, 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.Me, &data.ClientID, &scope)
err = row.Scan(&timeString, &data.token, &data.ClientID, &scope)
if err == sql.ErrNoRows {
return nil, errors.New("token not found")
} else if err != nil {

View File

@ -15,14 +15,11 @@
{{ end }}
</ul>
{{ end }}
<p><strong>me:</strong> {{ .Data.Me }}</p>
<p><strong>client_id:</strong> {{ .Data.ClientID }}</p>
<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="me" value="{{ .Data.Me }}">
<input type="hidden" name="response_type" value="{{ .Data.ResponseType }}">
<input type="submit" value="{{ string .Blog.Lang "authenticate" }}">
</form>
</div>