aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristine Dodrill <me@christine.website>2019-01-12 16:04:16 -0800
committerChristine Dodrill <me@christine.website>2019-01-12 16:04:16 -0800
commitf042c7fcf9989bbcc8ebb95e35c90de8b300e4b3 (patch)
treed2df7dc8d6fcdd66a9764a980bfdf8da1557838f
parentf1958d7425466a17c8f4cf6f8c11efe5f5091ad9 (diff)
downloadx-f042c7fcf9989bbcc8ebb95e35c90de8b300e4b3.tar.xz
x-f042c7fcf9989bbcc8ebb95e35c90de8b300e4b3.zip
experiment: idp for indieauth
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--idp/.gitignore4
-rw-r--r--idp/build.go39
-rw-r--r--idp/idpmiddleware/doc.go4
-rw-r--r--idp/idpmiddleware/middleware.go69
-rw-r--r--idp/main.go215
-rw-r--r--tools/appsluggr/main.go2
8 files changed, 336 insertions, 0 deletions
diff --git a/go.mod b/go.mod
index 0173d0e..fc626d8 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index 45537dc..8648fa5 100644
--- a/go.sum
+++ b/go.sum
@@ -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)