diff options
| author | Christine Dodrill <me@christine.website> | 2019-01-12 16:04:16 -0800 |
|---|---|---|
| committer | Christine Dodrill <me@christine.website> | 2019-01-12 16:04:16 -0800 |
| commit | f042c7fcf9989bbcc8ebb95e35c90de8b300e4b3 (patch) | |
| tree | d2df7dc8d6fcdd66a9764a980bfdf8da1557838f | |
| parent | f1958d7425466a17c8f4cf6f8c11efe5f5091ad9 (diff) | |
| download | x-f042c7fcf9989bbcc8ebb95e35c90de8b300e4b3.tar.xz x-f042c7fcf9989bbcc8ebb95e35c90de8b300e4b3.zip | |
experiment: idp for indieauth
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | idp/.gitignore | 4 | ||||
| -rw-r--r-- | idp/build.go | 39 | ||||
| -rw-r--r-- | idp/idpmiddleware/doc.go | 4 | ||||
| -rw-r--r-- | idp/idpmiddleware/middleware.go | 69 | ||||
| -rw-r--r-- | idp/main.go | 215 | ||||
| -rw-r--r-- | tools/appsluggr/main.go | 2 |
8 files changed, 336 insertions, 0 deletions
@@ -62,6 +62,7 @@ require ( github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b // indirect github.com/tjfoc/gmsm v1.0.1 // indirect github.com/tmc/scp v0.0.0-20170824174625-f7b48647feef + github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119 github.com/xtaci/kcp-go v5.0.7+incompatible github.com/xtaci/smux v1.1.0 github.com/yookoala/realpath v1.0.0 // indirect @@ -149,6 +149,8 @@ github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/ github.com/tmc/scp v0.0.0-20170824174625-f7b48647feef h1:7D6Nm4D6f0ci9yttWaKjM1TMAXrH5Su72dojqYGntFY= github.com/tmc/scp v0.0.0-20170824174625-f7b48647feef/go.mod h1:WLFStEdnJXpjK8kd4qKLwQKX/1vrDzp5BcDyiZJBHJM= github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89/go.mod h1:ejwOYCjnDMyO5LXFXRARQJGBZ6xQJZ3rgAHE5drSuMM= +github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119 h1:YyPWX3jLOtYKulBR6AScGIs74lLrJcgeKRwcbAuQOG4= +github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119/go.mod h1:/nuTSlK+okRfR/vnIPqR89fFKonnWPiZymN5ydRJkX8= github.com/xtaci/kcp-go v5.0.7+incompatible h1:zs9tc8XRID0m+aetu3qPWZFyRt2UIMqbXIBgw+vcnlE= github.com/xtaci/kcp-go v5.0.7+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE= github.com/xtaci/smux v1.1.0 h1:VO9uJ2TQprdETdlkV+MggqiKLmXvB9I9gZR0AG4RypY= diff --git a/idp/.gitignore b/idp/.gitignore new file mode 100644 index 0000000..72b5f4b --- /dev/null +++ b/idp/.gitignore @@ -0,0 +1,4 @@ +*.cfg +idp +web +*.tar.gz
\ No newline at end of file diff --git a/idp/build.go b/idp/build.go new file mode 100644 index 0000000..9d9d01d --- /dev/null +++ b/idp/build.go @@ -0,0 +1,39 @@ +//+build ignore + +// Builds and deploys the application to minipaas. +package main + +import ( + "context" + "log" + "os" + + "github.com/Xe/x/internal/greedo" + "github.com/Xe/x/internal/minipaas" + "github.com/Xe/x/internal/yeet" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + env := append(os.Environ(), []string{"CGO_ENABLED=0", "GOOS=linux"}...) + yeet.ShouldWork(ctx, env, yeet.WD, "vgo", "build", "-o=web") + yeet.ShouldWork(ctx, env, yeet.WD, "appsluggr", "-web=web") + fin, err := os.Open("slug.tar.gz") + if err != nil { + log.Fatal(err) + } + defer fin.Close() + + fname := "idp-" + yeet.DateTag + ".tar.gz" + pubURL, err := greedo.CopyFile(fname, fin) + if err != nil { + log.Fatal(err) + } + + err = minipaas.Exec("tar:from idp " + pubURL) + if err != nil { + log.Fatal(err) + } +} diff --git a/idp/idpmiddleware/doc.go b/idp/idpmiddleware/doc.go new file mode 100644 index 0000000..e4f06ee --- /dev/null +++ b/idp/idpmiddleware/doc.go @@ -0,0 +1,4 @@ +// Package idpmiddleware is a simple HTTP middleware that protects routes using +// idp(1). This only allows users that the given idp(1) instance is configured. +// to allow. +package idpmiddleware diff --git a/idp/idpmiddleware/middleware.go b/idp/idpmiddleware/middleware.go new file mode 100644 index 0000000..eaec2ce --- /dev/null +++ b/idp/idpmiddleware/middleware.go @@ -0,0 +1,69 @@ +package idpmiddleware + +import ( + "net/http" + "net/url" + "sync" + "time" + + "github.com/pborman/uuid" +) + +// Protect protects a given URL behind your given idp(1) server. +func Protect(idpServer, me, selfURL string) func(next http.Handler) http.Handler { + lock := sync.Mutex{} + codes := map[string]string{} + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/auth/challenge" { + v := r.URL.Query() + lock.Lock() + defer lock.Unlock() + if cd := v.Get("code"); codes[cd] == cd { + http.SetCookie(w, &http.Cookie{ + Name: "idp", + Value: me, + HttpOnly: true, + Expires: time.Now().Add(900 * time.Hour), + }) + + http.Redirect(w, r, selfURL, http.StatusTemporaryRedirect) + return + } + } + + cookie, err := r.Cookie("idp") + if err != nil { + u, err := url.Parse(idpServer) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + code := uuid.New() + lock.Lock() + codes[code] = code + lock.Unlock() + + u.Path = "/auth" + v := url.Values{} + v.Set("me", me) + v.Set("client_id", selfURL) + v.Set("redirect_uri", selfURL+"/auth/challenge") + v.Set("state", code) + v.Set("response_type", "id") + + http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect) + return + } + + if cookie.Value != me { + http.Error(w, "wrong identity", http.StatusBadRequest) + return + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/idp/main.go b/idp/main.go new file mode 100644 index 0000000..f19b690 --- /dev/null +++ b/idp/main.go @@ -0,0 +1,215 @@ +package main + +import ( + "encoding/json" + "flag" + "log" + "net/http" + "net/url" + "sync" + "text/template" + "time" + + "github.com/Xe/x/internal" + "github.com/pborman/uuid" + "github.com/xlzd/gotp" + "within.website/ln" + "within.website/ln/ex" +) + +var ( + domain = flag.String("domain", "idp.christine.website", "domain to be hosted from") + otpSecret = flag.String("otp-secret", "", "OTP secret") + port = flag.String("port", "5484", "TCP port to listen on for HTTP") + owner = flag.String("owner", "https://christine.website/", "the me=that is required") + secretGen = flag.Int("secret-gen", 0, "generate a secret of len if set") +) + +func main() { + internal.HandleStartup() + + if *secretGen != 0 { + log.Fatal(gotp.RandomSecret(*secretGen)) + } + + i := &idp{ + t: gotp.NewDefaultTOTP(*otpSecret), + bearer2me: map[string]string{}, + } + + log.Println(i.t.ProvisioningUri(*domain, *domain)) + + http.HandleFunc("/auth", i.auth) + http.HandleFunc("/challenge", i.challenge) + http.ListenAndServe(":"+*port, ex.HTTPLog(http.DefaultServeMux)) +} + +type idp struct { + t *gotp.TOTP + + sync.Mutex + bearer2me map[string]string +} + +// auth implements https://indieweb.org/authorization-endpoint#Open_Source_Authorization_Endpoints +func (i *idp) auth(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + log.Printf("idp: form error in /auth: %v", err) + return + } + + var ( + code, me, state, responseType, redirectURI, clientID string + ) + for k, v := range r.Form { + switch k { + case "state": + state = v[0] + case "me": + me = v[0] + case "response_type": + responseType = v[0] + case "redirect_uri": + redirectURI = v[0] + case "client_id": + clientID = v[0] + case "code": + code = v[0] + } + } + + if code != "" { + i.Lock() + person := i.bearer2me[code] + delete(i.bearer2me, code) + i.Unlock() + + ctx := r.Context() + ln.Log(ctx, ln.F{"state": state, "code": code, "accept": r.Header.Get("Accept"), "person": person}) + + w.Header().Set("Content-Type", r.Header.Get("Accept")) + switch r.Header.Get("Accept") { + case "application/x-www-form-urlencoded": + v := url.Values{} + v.Set("me", person) + + http.Error(w, v.Encode(), http.StatusOK) + case "application/json": + json.NewEncoder(w).Encode(struct { + Me string `json:"me"` + }{ + Me: person, + }) + } + + return + } + + if me != *owner { + http.Error(w, "Not allowed", http.StatusUnauthorized) + return + } + + t, err := template.New("auth").Parse(authPageTemplate) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = t.Execute(w, struct { + ClientID, State, Me, ResponseType, RedirectURI string + }{ + ClientID: clientID, + State: state, + Me: me, + ResponseType: responseType, + RedirectURI: redirectURI, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func (i *idp) challenge(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + log.Printf("idp: form error in /auth: %v", err) + return + } + + var ( + code, me, state, redirectURI string + ) + for k, v := range r.Form { + switch k { + case "code": + code = v[0] + case "state": + state = v[0] + case "me": + me = v[0] + case "redirect_uri": + redirectURI = v[0] + } + } + + nowCode := i.t.Now() + if code != nowCode { + http.Error(w, "Not allowed", http.StatusUnauthorized) + return + } + + bearerToken := uuid.New() + i.Lock() + i.bearer2me[bearerToken] = me + i.Unlock() + + u, err := url.Parse(redirectURI) + if err != nil { + http.Error(w, "url error", http.StatusBadRequest) + return + } + + time.Sleep(125 * time.Millisecond) + + q := u.Query() + q.Set("state", state) + q.Set("code", bearerToken) + u.RawQuery = q.Encode() + + http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect) +} + +const authPageTemplate = `<html> +<head> +<link rel="stylesheet" href="https://unpkg.com/chota@0.5.2/dist/chota.min.css"> +<title>Auth</title> +<style> +:root { + --color-primary: #da1d50; /* brand color */ + --grid-maxWidth: 40rem; +} +</style> +</head> +<body id="top"> +<div class="container"> +<div class="card"> + <header> + <h4>Log in to {{ .ClientID }} as {{ .Me }}</h4> + </header> + <p><form action="/challenge" method="GET"> + Code: <br> + <input type="text" name="code" value=""><br><br> + <input type="hidden" name="me" value="{{ .Me }}"> + <input type="hidden" name="state" value="{{ .State }}"> + <input type="hidden" name="client_id" value="{{ .ClientID }}"> + <input type="hidden" name="response_type" value="{{ .ResponseType }}"> + <input type="hidden" name="redirect_uri" value="{{ .RedirectURI }}"> + <input class="button primary" type="submit" value="Submit"> +</form></p> +</div> +</div> +</body> +</html>` diff --git a/tools/appsluggr/main.go b/tools/appsluggr/main.go index 2461786..94d2e10 100644 --- a/tools/appsluggr/main.go +++ b/tools/appsluggr/main.go @@ -60,6 +60,8 @@ func main() { Copy(*worker, filepath.Join(dir, "bin", "worker")) } + os.MkdirAll(filepath.Join(dir, ".config"), 0777) + err = ioutil.WriteFile(filepath.Join(dir, ".buildpacks"), []byte("https://github.com/ryandotsmith/null-buildpack"), 0666) if err != nil { log.Fatal(err) |
