From e611a5008ddc9eda5deadb2de8ee29ed4383294d Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Tue, 13 Oct 2020 21:35:39 +0200 Subject: [PATCH] IndieAuth server --- config.go | 9 +- databaseMigrations.go | 3 + example-config.yaml | 3 - go.mod | 6 +- go.sum | 12 +- http.go | 17 +- indieauth.go | 51 +----- indieauthserver.go | 296 +++++++++++++++++++++++++++++++++ templates/index.gohtml | 15 +- templates/indieauthflow.gohtml | 32 ++++ templates/micropub.gohtml | 11 +- templates/strings/default.yaml | 5 +- 12 files changed, 369 insertions(+), 91 deletions(-) create mode 100644 indieauthserver.go create mode 100644 templates/indieauthflow.gohtml diff --git a/config.go b/config.go index d6f856c..74720c4 100644 --- a/config.go +++ b/config.go @@ -120,9 +120,6 @@ type configMicropub struct { Enabled bool `mapstructure:"enabled"` Path string `mapstructure:"path"` AuthAllowed []string `mapstructure:"authAllowed"` - TokenEndpoint string `mapstructure:"tokenEndpoint"` - AuthEndpoint string `mapstructure:"authEndpoint"` - Authn string `mapstructure:"authn"` CategoryParam string `mapstructure:"categoryParam"` ReplyParam string `mapstructure:"replyParam"` LikeParam string `mapstructure:"likeParam"` @@ -157,9 +154,6 @@ func initConfig() error { viper.SetDefault("hugo.frontmatter", []*frontmatter{{Meta: "title", Parameter: "title"}, {Meta: "tags", Parameter: "tags"}}) viper.SetDefault("micropub.enabled", true) viper.SetDefault("micropub.path", "/micropub") - viper.SetDefault("micropub.authAllowed", []string{}) - viper.SetDefault("micropub.tokenEndpoint", "https://tokens.indieauth.com/token") - viper.SetDefault("micropub.authEndpoint", "https://indieauth.com/auth") viper.SetDefault("micropub.categoryParam", "tags") viper.SetDefault("micropub.replyParam", "replylink") viper.SetDefault("micropub.likeParam", "likelink") @@ -179,5 +173,8 @@ func initConfig() error { if len(appConfig.DefaultBlog) == 0 || appConfig.Blogs[appConfig.DefaultBlog] == nil { return errors.New("no default blog or default blog not present") } + if len(appConfig.Micropub.AuthAllowed) == 0 { + appConfig.Micropub.AuthAllowed = []string{appConfig.Server.Domain} + } return nil } diff --git a/databaseMigrations.go b/databaseMigrations.go index 55f0a4c..16cfdb6 100644 --- a/databaseMigrations.go +++ b/databaseMigrations.go @@ -19,6 +19,9 @@ func migrateDb() error { CREATE TABLE post_parameters (id integer primary key autoincrement, path text not null, parameter text not null, value text); CREATE INDEX index_pp_path on post_parameters (path); CREATE TABLE redirects (fromPath text not null, toPath text not null, primary key (fromPath, toPath)); + CREATE TABLE indieauthauth (time text not null, code text not null, me text not null, client text not null, redirect text not null, scope text not null); + CREATE TABLE indieauthtoken (time text not null, token text not null, me text not null, client text not null, scope text not null); + CREATE INDEX index_iat_token on indieauthtoken (token); `) return err }, diff --git a/example-config.yaml b/example-config.yaml index 6b8b3e1..acdd7aa 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -58,9 +58,6 @@ micropub: path: /micropub authAllowed: - example.com - tokenEndpoint: https://tokens.indieauth.com/token - authEndpoint: https://indieauth.com/auth - authn: login@example.com categoryParam: tags replyParam: replylink likeParam: likelink diff --git a/go.mod b/go.mod index 4617e72..6166b60 100644 --- a/go.mod +++ b/go.mod @@ -39,12 +39,12 @@ require ( github.com/yuin/goldmark-emoji v1.0.1 go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.16.0 // indirect - golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect + golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee // indirect golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/net v0.0.0-20201010224723-4f7140c49acb // indirect golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520 // indirect - golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 // indirect - golang.org/x/tools v0.0.0-20201011145850-ed2f50202694 // indirect + golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb // indirect + golang.org/x/tools v0.0.0-20201013183236-0112737ef124 // indirect gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect diff --git a/go.sum b/go.sum index e335ee3..5084ebf 100644 --- a/go.sum +++ b/go.sum @@ -331,8 +331,8 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -414,8 +414,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c= golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb h1:HS9IzC4UFbpMBLQUDSQcU+ViVT1vdFCQVjdPVpTlZrs= +golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -447,8 +447,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn 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-20201011145850-ed2f50202694 h1:BANdcOVw3KTuUiyfDp7wrzCpkCe8UP3lowugJngxBTg= -golang.org/x/tools v0.0.0-20201011145850-ed2f50202694/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201013183236-0112737ef124 h1:CQRvWGvGfDDk3OMpDX19jvU16tKf5RlqlJc+ki94ZJs= +golang.org/x/tools v0.0.0-20201013183236-0112737ef124/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/http.go b/http.go index cc77f9c..44164fd 100644 --- a/http.go +++ b/http.go @@ -70,11 +70,13 @@ func buildHandler() (http.Handler, error) { r.Mount("/debug", middleware.Profiler()) } + authMiddleware := middleware.BasicAuth("API", map[string]string{ + appConfig.User.Nick: appConfig.User.Password, + }) + // API r.Route("/api", func(apiRouter chi.Router) { - apiRouter.Use(middleware.BasicAuth("API", map[string]string{ - appConfig.User.Nick: appConfig.User.Password, - })) + apiRouter.Use(authMiddleware) apiRouter.Post("/post", apiPostCreate) apiRouter.Get("/post", apiPostRead) apiRouter.Delete("/post", apiPostDelete) @@ -88,6 +90,15 @@ func buildHandler() (http.Handler, error) { r.With(checkIndieAuth).Post(appConfig.Micropub.Path+micropubMediaSubPath, serveMicropubMedia) } + // IndieAuth + r.Route("/indieauth", func(indieauthRouter chi.Router) { + indieauthRouter.With(authMiddleware).Get("/", indieAuthAuth) + indieauthRouter.With(authMiddleware).Post("/accept", indieAuthAccept) + indieauthRouter.Post("/", indieAuthAuth) + indieauthRouter.Get("/token", indieAuthToken) + indieauthRouter.Post("/token", indieAuthToken) + }) + // Posts allPostPaths, err := allPostPaths() if err != nil { diff --git a/indieauth.go b/indieauth.go index 5891df3..1b4c516 100644 --- a/indieauth.go +++ b/indieauth.go @@ -1,44 +1,26 @@ package main import ( - "errors" "fmt" "net/http" "net/url" "strings" - "time" ) -type indieAuthTokenResponse struct { - Me string `json:"me"` - ClientID string `json:"client_id"` - Scope string `json:"scope"` - IssuedBy string `json:"issued_by"` - Error string `json:"error"` - ErrorDescription string `json:"error_description"` - StatusCode int -} - func 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 { - if accessToken := r.URL.Query().Get("access_token"); len(accessToken) > 0 { - bearerToken = "Bearer " + accessToken - } + bearerToken = r.URL.Query().Get("access_token") } - tokenResponse, err := verifyIndieAuthAccessToken(bearerToken) + tokenData, err := verifyIndieAuthToken(bearerToken) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } - if tokenResponse.StatusCode != http.StatusOK { - http.Error(w, "Failed to retrieve authentication information", http.StatusUnauthorized) - return - } authorized := false for _, allowed := range appConfig.Micropub.AuthAllowed { - if err := compareHostnames(tokenResponse.Me, allowed); err == nil { + if err := compareHostnames(tokenData.Me, allowed); err == nil { authorized = true break } @@ -52,33 +34,6 @@ func checkIndieAuth(next http.Handler) http.Handler { }) } -func verifyIndieAuthAccessToken(bearerToken string) (*indieAuthTokenResponse, error) { - if len(bearerToken) == 0 { - return nil, errors.New("no token") - } - req, err := http.NewRequest("GET", appConfig.Micropub.TokenEndpoint, nil) - if err != nil { - return nil, err - } - req.Header.Add(contentType, contentTypeWWWForm) - req.Header.Add("Authorization", bearerToken) - req.Header.Add("Accept", contentTypeJSON) - c := http.Client{ - Timeout: time.Duration(10 * time.Second), - } - resp, err := c.Do(req) - if err != nil { - return nil, err - } - tokenRes := indieAuthTokenResponse{StatusCode: resp.StatusCode} - err = json.NewDecoder(resp.Body).Decode(&tokenRes) - resp.Body.Close() - if err != nil { - return nil, err - } - return &tokenRes, nil -} - func compareHostnames(a string, allowed string) error { h1, err := url.Parse(a) if err != nil { diff --git a/indieauthserver.go b/indieauthserver.go new file mode 100644 index 0000000..36a26cf --- /dev/null +++ b/indieauthserver.go @@ -0,0 +1,296 @@ +package main + +import ( + "crypto/sha1" + "database/sql" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/spf13/cast" +) + +// https://www.w3.org/TR/indieauth/ + +type indieAuthData struct { + Me string + ClientID string + RedirectURI string + State string + ResponseType string + Scopes []string + code string + token string + time time.Time +} + +func indieAuthAuth(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + // Authentication / 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"), + } + if data.ResponseType == "" { + data.ResponseType = "id" + } + 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) + 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, "indieauthflow", &renderData{ + Data: data, + }) + } else if r.Method == http.MethodPost { + // 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 { + return false + } + if u.Scheme != "http" && u.Scheme != "https" { + return false + } + // Missing: Check path + // Missing: Check single/double dot path + if u.Fragment != "" { + return false + } + if u.User.String() != "" { + return false + } + if u.Port() != "" { + return false + } + // Missing: Check domain / ip + return true +} + +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(), + } + sha := sha1.New() + sha.Write([]byte(data.time.String() + data.Me + data.ClientID)) + data.code = fmt.Sprintf("%x", sha.Sum(nil)) + err := data.saveAuthorization() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + http.Redirect(w, r, data.RedirectURI+"?code="+data.code+"&state="+data.State, 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"` +} + +func indieAuthToken(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + // Token verification + data, err := verifyIndieAuthToken(r.Header.Get("Authorization")) + if err != nil { + http.Error(w, "Invalid token or token not found", http.StatusUnauthorized) + } + res := &tokenResponse{ + Scope: strings.Join(data.Scopes, " "), + Me: data.Me, + ClientID: data.ClientID, + } + 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 + } + } else if r.Method == http.MethodPost { + r.ParseForm() + // Token Revocation + if r.Form.Get("action") == "revoke" { + 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"), + Me: r.Form.Get("me"), + } + valid, err := data.verifyAuthorization(false) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if !valid { + http.Error(w, "Authentication not valid", http.StatusForbidden) + return + } + if len(data.Scopes) < 1 { + http.Error(w, "No scope", http.StatusBadRequest) + return + } + data.time = time.Now() + sha := sha1.New() + sha.Write([]byte(data.time.String() + data.Me + data.ClientID)) + data.token = fmt.Sprintf("%x", sha.Sum(nil)) + err = data.saveToken() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + res := &tokenResponse{ + TokenType: "Bearer", + AccessToken: data.token, + Scope: strings.Join(data.Scopes, " "), + 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 (data *indieAuthData) saveAuthorization() (err error) { + startWritingToDb() + defer finishWritingToDb() + _, err = appDb.Exec("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, " ")) + return +} + +func (data *indieAuthData) verifyAuthorization(authentication bool) (valid bool, err error) { + // code valid for 600 seconds + if !authentication { + row := appDb.QueryRow("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) + 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 := appDb.QueryRow("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) + 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 + } + } + valid = true + startWritingToDb() + defer finishWritingToDb() + _, err = appDb.Exec("delete from indieauthauth where code = ? or time < ?", data.code, time.Now().Unix()-600) + data.code = "" + return +} + +func (data *indieAuthData) saveToken() (err error) { + startWritingToDb() + defer finishWritingToDb() + _, err = appDb.Exec("insert into indieauthtoken (time, token, me, client, scope) values (?, ?, ?, ?, ?)", data.time.Unix(), data.token, data.Me, data.ClientID, strings.Join(data.Scopes, " ")) + return +} + +func verifyIndieAuthToken(token string) (data *indieAuthData, err error) { + token = strings.ReplaceAll(token, "Bearer ", "") + data = &indieAuthData{} + row := appDb.QueryRow("select time, token, me, client, scope from indieauthtoken where token = ?", token) + timeString := "" + scope := "" + err = row.Scan(&timeString, &data.token, &data.Me, &data.ClientID, &scope) + if err == sql.ErrNoRows { + return nil, errors.New("token not found") + } else if err != nil { + return nil, err + } + if scope != "" { + data.Scopes = strings.Split(scope, " ") + } + data.time = time.Unix(cast.ToInt64(timeString), 0) + return +} + +func revokeIndieAuthToken(token string) { + if token == "" { + return + } + startWritingToDb() + defer finishWritingToDb() + _, _ = appDb.Exec("delete from indieauthtoken where token=?", token) + return +} diff --git a/templates/index.gohtml b/templates/index.gohtml index 5d6f3f6..4e31dd9 100644 --- a/templates/index.gohtml +++ b/templates/index.gohtml @@ -4,18 +4,9 @@ {{ else }} {{ .Blog.Title }} {{ end }} - - - + + + {{ end }} {{ define "main" }} diff --git a/templates/indieauthflow.gohtml b/templates/indieauthflow.gohtml new file mode 100644 index 0000000..c7ec7d4 --- /dev/null +++ b/templates/indieauthflow.gohtml @@ -0,0 +1,32 @@ +{{ define "title" }} + {{ string .Blog.Lang "indieauth" }} - {{ .Blog.Title }} +{{ end }} + +{{ define "main" }} +
+

{{ string .Blog.Lang "indieauth" }}

+
+ {{ if .Data.Scopes }} +

{{ string .Blog.Lang "scopes" }}

+
    + {{ range $i, $scope := .Data.Scopes }} +
  • + {{ end }} +
+ {{ end }} +

me: {{ .Data.Me }}

+

client_id: {{ .Data.ClientID }}

+

redirect_uri: {{ .Data.RedirectURI }}

+ + + + + + +
+
+{{ end }} + +{{ define "indieauthflow" }} + {{ template "base" . }} +{{ end }} \ No newline at end of file diff --git a/templates/micropub.gohtml b/templates/micropub.gohtml index 01c3798..b47ca91 100644 --- a/templates/micropub.gohtml +++ b/templates/micropub.gohtml @@ -2,13 +2,6 @@ {{ with micropub.Path }} {{ end }} - {{ with micropub.AuthEndpoint }} - - {{ end }} - {{ with micropub.TokenEndpoint }} - - {{ end }} - {{ with micropub.Authn }} - - {{ end }} + + {{ end }} \ No newline at end of file diff --git a/templates/strings/default.yaml b/templates/strings/default.yaml index d3cc5e9..bb50490 100644 --- a/templates/strings/default.yaml +++ b/templates/strings/default.yaml @@ -2,4 +2,7 @@ publishedon: "Published on" updatedon: "Updated on" next: "Next" prev: "Previous" -view: "View" \ No newline at end of file +view: "View" +authenticate: "Authenticate" +scopes: "Scopes" +indieauth: "IndieAuth" \ No newline at end of file