repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
907044e
parent
d7d2aff
author
Antonio Mika
date
2023-10-07 02:42:30 +0000 UTC
Implement a fake oauth login system that uses ssh generated tokens (#44)

14 files changed,  +645, -37
M Makefile
+1, -1
1@@ -34,7 +34,7 @@ bp-caddy: bp-setup
2 .PHONY: bp-caddy
3 
4 bp-auth: bp-setup
5-	$(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/auth:$(DOCKER_TAG) -f auth/Dockerfile .
6+	$(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/auth:$(DOCKER_TAG) --build-arg APP=auth --target release-web .
7 .PHONY: bp-auth
8 
9 bp-bouncer: bp-setup
M auth/auth.go
+120, -4
  1@@ -4,7 +4,9 @@ import (
  2 	"context"
  3 	"encoding/json"
  4 	"fmt"
  5+	"html/template"
  6 	"net/http"
  7+	"net/url"
  8 	"strings"
  9 
 10 	"github.com/picosh/pico/db"
 11@@ -29,22 +31,27 @@ type oauth2Server struct {
 12 	Issuer                                    string   `json:"issuer"`
 13 	IntrospectionEndpoint                     string   `json:"introspection_endpoint"`
 14 	IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported"`
 15+	AuthorizationEndpoint                     string   `json:"authorization_endpoint"`
 16+	TokenEndpoint                             string   `json:"token_endpoint"`
 17+	ResponseTypesSupported                    []string `json:"response_types_supported"`
 18 }
 19 
 20-func getIntrospectURL(cfg *AuthCfg) string {
 21-	return fmt.Sprintf("%s/introspect", cfg.Domain)
 22+func generateURL(cfg *AuthCfg, path string) string {
 23+	return fmt.Sprintf("%s/%s", cfg.Domain, path)
 24 }
 25 
 26 func wellKnownHandler(w http.ResponseWriter, r *http.Request) {
 27 	client := getClient(r)
 28-	introspectURL := getIntrospectURL(client.Cfg)
 29 
 30 	p := oauth2Server{
 31 		Issuer:                client.Cfg.Issuer,
 32-		IntrospectionEndpoint: introspectURL,
 33+		IntrospectionEndpoint: generateURL(client.Cfg, "introspect"),
 34 		IntrospectionEndpointAuthMethodsSupported: []string{
 35 			"none",
 36 		},
 37+		AuthorizationEndpoint:  generateURL(client.Cfg, "authorize"),
 38+		TokenEndpoint:          generateURL(client.Cfg, "token"),
 39+		ResponseTypesSupported: []string{"code"},
 40 	}
 41 	w.Header().Set("Content-Type", "application/json")
 42 	w.WriteHeader(http.StatusOK)
 43@@ -85,10 +92,119 @@ func introspectHandler(w http.ResponseWriter, r *http.Request) {
 44 	}
 45 }
 46 
 47+func authorizeHandler(w http.ResponseWriter, r *http.Request) {
 48+	client := getClient(r)
 49+
 50+	responseType := r.URL.Query().Get("response_type")
 51+	clientID := r.URL.Query().Get("client_id")
 52+	redirectURI := r.URL.Query().Get("redirect_uri")
 53+	scope := r.URL.Query().Get("scope")
 54+
 55+	client.Logger.Infof("authorize handler (%s, %s, %s, %s)", responseType, clientID, redirectURI, scope)
 56+
 57+	ts, err := template.ParseFiles(
 58+		"auth/html/redirect.page.tmpl",
 59+		"auth/html/footer.partial.tmpl",
 60+		"auth/html/marketing-footer.partial.tmpl",
 61+		"auth/html/base.layout.tmpl",
 62+	)
 63+
 64+	if err != nil {
 65+		client.Logger.Error(err)
 66+		http.Error(w, err.Error(), http.StatusUnauthorized)
 67+		return
 68+	}
 69+
 70+	err = ts.Execute(w, map[string]any{
 71+		"response_type": responseType,
 72+		"client_id":     clientID,
 73+		"redirect_uri":  redirectURI,
 74+		"scope":         scope,
 75+	})
 76+
 77+	if err != nil {
 78+		client.Logger.Error(err)
 79+		http.Error(w, err.Error(), http.StatusUnauthorized)
 80+		return
 81+	}
 82+}
 83+
 84+func redirectHandler(w http.ResponseWriter, r *http.Request) {
 85+	client := getClient(r)
 86+
 87+	token := r.FormValue("token")
 88+	redirectURI := r.FormValue("redirect_uri")
 89+	responseType := r.FormValue("response_type")
 90+
 91+	client.Logger.Infof("redirect handler (%s, %s, %s)", token, redirectURI, responseType)
 92+
 93+	if token == "" || redirectURI == "" || responseType != "code" {
 94+		http.Error(w, "bad request", http.StatusBadRequest)
 95+		return
 96+	}
 97+
 98+	url, err := url.Parse(redirectURI)
 99+	if err != nil {
100+		http.Error(w, err.Error(), http.StatusBadRequest)
101+		return
102+	}
103+
104+	urlQuery := url.Query()
105+	urlQuery.Add("code", token)
106+
107+	url.RawQuery = urlQuery.Encode()
108+
109+	http.Redirect(w, r, url.String(), http.StatusFound)
110+}
111+
112+type oauth2Token struct {
113+	AccessToken string `json:"access_token"`
114+}
115+
116+func tokenHandler(w http.ResponseWriter, r *http.Request) {
117+	client := getClient(r)
118+
119+	token := r.FormValue("code")
120+	redirectURI := r.FormValue("redirect_uri")
121+	grantType := r.FormValue("grant_type")
122+
123+	client.Logger.Infof("handle token (%s, %s, %s)", token, redirectURI, grantType)
124+
125+	_, err := client.Dbpool.FindUserForToken(token)
126+	if err != nil {
127+		client.Logger.Error(err)
128+		http.Error(w, err.Error(), http.StatusUnauthorized)
129+		return
130+	}
131+
132+	p := oauth2Token{
133+		AccessToken: token,
134+	}
135+	w.Header().Set("Content-Type", "application/json")
136+	w.WriteHeader(http.StatusOK)
137+	err = json.NewEncoder(w).Encode(p)
138+	if err != nil {
139+		client.Logger.Error(err)
140+		http.Error(w, err.Error(), http.StatusInternalServerError)
141+	}
142+}
143+
144 func createMainRoutes() []shared.Route {
145+	fileServer := http.FileServer(http.Dir("auth/public"))
146+
147 	routes := []shared.Route{
148 		shared.NewRoute("GET", "/.well-known/oauth-authorization-server", wellKnownHandler),
149 		shared.NewRoute("POST", "/introspect", introspectHandler),
150+		shared.NewRoute("GET", "/authorize", authorizeHandler),
151+		shared.NewRoute("POST", "/token", tokenHandler),
152+		shared.NewRoute("POST", "/redirect", redirectHandler),
153+		shared.NewRoute("GET", "/main.css", fileServer.ServeHTTP),
154+		shared.NewRoute("GET", "/card.png", fileServer.ServeHTTP),
155+		shared.NewRoute("GET", "/favicon-16x16.png", fileServer.ServeHTTP),
156+		shared.NewRoute("GET", "/favicon-32x32.png", fileServer.ServeHTTP),
157+		shared.NewRoute("GET", "/apple-touch-icon.png", fileServer.ServeHTTP),
158+		shared.NewRoute("GET", "/favicon.ico", fileServer.ServeHTTP),
159+		shared.NewRoute("GET", "/robots.txt", fileServer.ServeHTTP),
160 	}
161 
162 	return routes
D auth/html/.gitkeep
+0, -0
A auth/html/base.layout.tmpl
+19, -0
 1@@ -0,0 +1,19 @@
 2+{{define "base"}}
 3+<!doctype html>
 4+<html lang="en">
 5+  <head>
 6+    <meta charset='utf-8'>
 7+    <meta name="viewport" content="width=device-width, initial-scale=1" />
 8+    <title>{{template "title" .}}</title>
 9+
10+    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
11+
12+    <meta name="keywords" content="static, site, hosting" />
13+
14+    <link rel="stylesheet" href="/main.css" />
15+
16+    {{template "meta" .}}
17+  </head>
18+  <body {{template "attrs" .}}>{{template "body" .}}</body>
19+</html>
20+{{end}}
A auth/html/footer.partial.tmpl
+3, -0
1@@ -0,0 +1,3 @@
2+{{define "footer"}}
3+<hr />
4+{{end}}
A auth/html/marketing-footer.partial.tmpl
+9, -0
 1@@ -0,0 +1,9 @@
 2+{{define "marketing-footer"}}
 3+<footer>
 4+  <hr />
 5+  <p class="font-italic">Built and maintained by <a href="https://pico.sh">pico.sh</a>.</p>
 6+  <div>
 7+    <a href="https://github.com/picosh/pico">source</a>
 8+  </div>
 9+</footer>
10+{{end}}
A auth/html/redirect.page.tmpl
+64, -0
 1@@ -0,0 +1,64 @@
 2+{{template "base" .}}
 3+
 4+{{define "title"}}auth redirect{{end}}
 5+
 6+{{define "meta"}}{{end}}
 7+
 8+{{define "attrs"}}{{end}}
 9+
10+{{define "body"}}
11+<header>
12+    <h1 class="text-2xl">Auth Redirect</h1>
13+    <hr />
14+</header>
15+<main>
16+    <section>
17+        <h2 class="text-xl">You are being redirected to {{.redirect_uri}}</h2>
18+        <p>
19+            Here is their auth request data:
20+        </p>
21+
22+        <article>
23+            <h2 class="text-lg">Client ID</h2>
24+            <div>{{.client_id}}</div>
25+        </article>
26+
27+        <br />
28+
29+        <article>
30+            <h2 class="text-lg">Redirect URI</h2>
31+            <div>{{.redirect_uri}}</div>
32+        </article>
33+
34+        <br />
35+
36+        <article>
37+            <h2 class="text-lg">Scope</h2>
38+            <div>{{.scope}}</div>
39+        </article>
40+
41+        <br />
42+
43+        <article>
44+            <h2 class="text-lg">Response Type</h2>
45+            <div>{{.response_type}}</div>
46+        </article>
47+
48+        <br />
49+
50+        <article>
51+            <h2 class="text-lg">If you would like to continue authenticating with this service, ssh into a pico service and generate a token. Then input it here and click submit.</h2>
52+            <br />
53+            <form action="/redirect" method="POST">
54+                <label for="token">Auth Token:</label><br>
55+                <input type="text" id="token" name="token"><br>
56+                <br />
57+                <input type="hidden" id="redirect_uri" name="redirect_uri" value="{{.redirect_uri}}">
58+                <input type="hidden" id="response_type" name="response_type" value="{{.response_type}}">
59+                <input type="submit" value="Submit">
60+            </form>
61+        </article>
62+    </section>
63+</main>
64+{{template "marketing-footer" .}}
65+{{end}}
D auth/public/.gitkeep
+0, -0
A auth/public/apple-touch-icon.png
+0, -0
A auth/public/favicon-16x16.png
+0, -0
A auth/public/favicon.ico
+0, -0
A auth/public/main.css
+388, -0
  1@@ -0,0 +1,388 @@
  2+*,
  3+::before,
  4+::after {
  5+  box-sizing: border-box;
  6+}
  7+
  8+::-moz-focus-inner {
  9+  border-style: none;
 10+  padding: 0;
 11+}
 12+:-moz-focusring {
 13+  outline: 1px dotted ButtonText;
 14+}
 15+:-moz-ui-invalid {
 16+  box-shadow: none;
 17+}
 18+
 19+@media (prefers-color-scheme: light) {
 20+  :root {
 21+    --white: #6a737d;
 22+    --code: #fff8d3;
 23+    --code-border: #f0d547;
 24+    --pre: #f6f8fa;
 25+    --bg-color: #fff;
 26+    --text-color: #24292f;
 27+    --link-color: #005cc5;
 28+    --visited: #6f42c1;
 29+    --blockquote: #005cc5;
 30+    --blockquote-bg: #fff;
 31+    --hover: #d73a49;
 32+    --grey: #ccc;
 33+  }
 34+}
 35+
 36+@media (prefers-color-scheme: dark) {
 37+  :root {
 38+    --white: #f2f2f2;
 39+    --code: #414558;
 40+    --code-border: #252525;
 41+    --pre: #252525;
 42+    --bg-color: #282a36;
 43+    --text-color: #f2f2f2;
 44+    --link-color: #8be9fd;
 45+    --visited: #bd93f9;
 46+    --blockquote: #bd93f9;
 47+    --blockquote-bg: #414558;
 48+    --hover: #ff80bf;
 49+    --grey: #414558;
 50+  }
 51+}
 52+
 53+html {
 54+  background-color: var(--bg-color);
 55+  color: var(--text-color);
 56+  line-height: 1.5;
 57+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
 58+    Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
 59+    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 60+  -webkit-text-size-adjust: 100%;
 61+  -moz-tab-size: 4;
 62+  tab-size: 4;
 63+}
 64+
 65+body {
 66+  margin: 0 auto;
 67+  max-width: 720px;
 68+}
 69+
 70+img {
 71+  max-width: 100%;
 72+  height: auto;
 73+}
 74+
 75+b,
 76+strong {
 77+  font-weight: bold;
 78+}
 79+
 80+code,
 81+kbd,
 82+samp,
 83+pre {
 84+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
 85+    monospace;
 86+  font-size: 0.8rem;
 87+}
 88+
 89+code,
 90+kbd,
 91+samp {
 92+  background-color: var(--code);
 93+  border: 1px solid var(--code-border);
 94+}
 95+
 96+pre > code {
 97+  background-color: inherit;
 98+  padding: 0;
 99+  border: none;
100+}
101+
102+code {
103+  border-radius: 0.3rem;
104+  padding: 0.15rem 0.2rem 0.05rem;
105+}
106+
107+pre {
108+  border-radius: 5px;
109+  padding: 1rem;
110+  margin: 1rem 0;
111+  overflow-x: auto;
112+  background-color: var(--pre) !important;
113+}
114+
115+small {
116+  font-size: 0.8rem;
117+}
118+
119+summary {
120+  display: list-item;
121+}
122+
123+h1,
124+h2,
125+h3 {
126+  margin: 0;
127+  padding: 0.6rem 0 0 0;
128+  border: 0;
129+  font-style: normal;
130+  font-weight: inherit;
131+  font-size: inherit;
132+}
133+
134+h1 > code {
135+  font-size: inherit;
136+}
137+
138+h2 > code {
139+  font-size: inherit;
140+}
141+
142+h3 > code {
143+  font-size: inherit;
144+}
145+
146+hr {
147+  color: inherit;
148+  border: 0;
149+  margin: 0;
150+  height: 1px;
151+  background: var(--grey);
152+  margin: 2rem auto;
153+  text-align: center;
154+}
155+
156+a {
157+  text-decoration: underline;
158+  color: var(--link-color);
159+}
160+
161+a:hover,
162+a:visited:hover {
163+  color: var(--hover);
164+}
165+
166+a:visited {
167+  color: var(--visited);
168+}
169+
170+a.link-grey {
171+  text-decoration: underline;
172+  color: var(--white);
173+}
174+
175+a.link-grey:visited {
176+  color: var(--white);
177+}
178+
179+section {
180+  margin-bottom: 1.4rem;
181+}
182+
183+section:last-child {
184+  margin-bottom: 0;
185+}
186+
187+header {
188+  margin: 1rem auto;
189+}
190+
191+p {
192+  margin: 0.8rem 0;
193+}
194+
195+article {
196+  overflow-wrap: break-word;
197+}
198+
199+blockquote {
200+  border-left: 5px solid var(--blockquote);
201+  background-color: var(--blockquote-bg);
202+  padding: 0.8rem;
203+  margin: 1rem 0;
204+}
205+
206+blockquote > p {
207+  margin: 0;
208+}
209+
210+ul,
211+ol {
212+  padding: 0 0 0 2rem;
213+  list-style-position: outside;
214+}
215+
216+ul[style*="list-style-type: none;"] {
217+  padding: 0;
218+}
219+
220+li {
221+  margin: 0.5rem 0;
222+}
223+
224+li > pre {
225+  padding: 0;
226+}
227+
228+footer {
229+  text-align: center;
230+  margin-bottom: 4rem;
231+}
232+
233+dt {
234+  font-weight: bold;
235+}
236+
237+dd {
238+  margin-left: 0;
239+}
240+
241+dd:not(:last-child) {
242+  margin-bottom: 0.5rem;
243+}
244+
245+figure {
246+  margin: 0;
247+}
248+
249+.post-date {
250+  width: 130px;
251+}
252+
253+.text-grey {
254+  color: var(--grey);
255+}
256+
257+.text-2xl {
258+  font-size: 1.85rem;
259+  line-height: 1.15;
260+}
261+
262+.text-xl {
263+  font-size: 1.55rem;
264+  line-height: 1.15;
265+}
266+
267+.text-lg {
268+  font-size: 1.35rem;
269+  line-height: 1.15;
270+}
271+
272+.text-md {
273+  font-size: 1.15rem;
274+  line-height: 1.15;
275+}
276+
277+.text-sm {
278+  font-size: 0.875rem;
279+}
280+
281+.text-center {
282+  text-align: center;
283+}
284+
285+.font-bold {
286+  font-weight: bold;
287+}
288+
289+.font-italic {
290+  font-style: italic;
291+}
292+
293+.inline {
294+  display: inline;
295+}
296+
297+.flex {
298+  display: flex;
299+}
300+
301+.items-center {
302+  align-items: center;
303+}
304+
305+.m-0 {
306+  margin: 0;
307+}
308+
309+.mt {
310+  margin-top: 0.5rem;
311+}
312+
313+.mb {
314+  margin-bottom: 0.5rem;
315+}
316+
317+.mr {
318+  margin-right: 0.5rem;
319+}
320+
321+.ml {
322+  margin-left: 0.5rem;
323+}
324+
325+.my {
326+  margin-top: 0.5rem;
327+  margin-bottom: 0.5rem;
328+}
329+
330+.my-2 {
331+  margin-top: 1rem;
332+  margin-bottom: 1rem;
333+}
334+
335+.mx {
336+  margin-left: 0.5rem;
337+  margin-right: 0.5rem;
338+}
339+
340+.mx-2 {
341+  margin-left: 1rem;
342+  margin-right: 1rem;
343+}
344+
345+.justify-between {
346+  justify-content: space-between;
347+}
348+
349+.flex-1 {
350+  flex: 1;
351+}
352+
353+.layout-aside {
354+  max-width: 50rem;
355+}
356+
357+.layout-aside aside {
358+  width: 200px;
359+}
360+
361+.layout-aside img {
362+  border-radius: 5px;
363+}
364+
365+#readme {
366+  display: none;
367+}
368+
369+@media only screen and (max-width: 600px) {
370+  body {
371+    padding: 1rem;
372+  }
373+
374+  header {
375+    margin: 0;
376+  }
377+
378+  .layout-aside main {
379+    flex-direction: column;
380+  }
381+
382+  aside {
383+    display: none;
384+  }
385+
386+  #readme {
387+    display: block;
388+  }
389+}
A auth/public/robots.txt
+2, -0
1@@ -0,0 +1,2 @@
2+User-agent: *
3+Allow: /
M caddy/Caddyfile.auth
+39, -32
 1@@ -59,50 +59,57 @@
 2 }
 3 
 4 *.pico.sh, pico.sh {
 5-       @auth {
 6-               host auth.pico.sh
 7-       }
 8+	@auth {
 9+		host auth.pico.sh
10+	}
11 
12-       @irc {
13-               host irc.pico.sh
14-       }
15+	@irc {
16+		host irc.pico.sh
17+	}
18 
19-       reverse_proxy @auth auth-web:3000
20+	@options {
21+		method OPTIONS
22+	}
23+	respond @options 204
24 
25-       reverse_proxy @irc https://bouncer:8080 {
26-               transport http {
27-                       tls_insecure_skip_verify
28-               }
29-       }
30+	reverse_proxy @auth auth-web:3000
31 
32-       tls {$APP_EMAIL} {
33-               dns cloudflare {$CF_API_TOKEN}
34-               resolvers 1.1.1.1
35-       }
36-       encode zstd gzip
37+	reverse_proxy @irc https://bouncer:8080 {
38+		transport http {
39+			tls_insecure_skip_verify
40+		}
41+	}
42 
43-       header {
44-               # disable FLoC tracking
45-               Permissions-Policy interest-cohort=()
46+	tls {$APP_EMAIL} {
47+		dns cloudflare {$CF_API_TOKEN}
48+		resolvers 1.1.1.1
49+	}
50+	encode zstd gzip
51 
52-               # enable HSTS
53-               Strict-Transport-Security max-age=31536000;
54+	header {
55+		# disable FLoC tracking
56+		Permissions-Policy interest-cohort=()
57 
58-               # disable clients from sniffing the media type
59-               X-Content-Type-Options nosniff
60+		# enable HSTS
61+		Strict-Transport-Security max-age=31536000;
62+
63+		# disable clients from sniffing the media type
64+		X-Content-Type-Options nosniff
65+
66+		# clickjacking protection
67+		X-Frame-Options DENY
68 
69-               # clickjacking protection
70-               X-Frame-Options DENY
71+		# keep referrer data off of HTTP connections
72+		Referrer-Policy no-referrer-when-downgrade
73 
74-               # keep referrer data off of HTTP connections
75-               Referrer-Policy no-referrer-when-downgrade
76+		Content-Security-Policy "default-src 'self'; img-src * 'unsafe-inline'; style-src * 'unsafe-inline'"
77 
78-               Content-Security-Policy "default-src 'self'; img-src * 'unsafe-inline'; style-src * 'unsafe-inline'"
79+		X-XSS-Protection "1; mode=block"
80 
81-               X-XSS-Protection "1; mode=block"
82+		Access-Control-Allow-Origin "https://chat.pico.sh"
83 
84-			   Access-Control-Allow-Origin "https://chat.pico.sh"
85-       }
86+		Access-Control-Allow-Headers "*"
87+	}
88 }
89 
90 :443 {