- 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
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
+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
+0,
-0
+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}}
1@@ -0,0 +1,3 @@
2+{{define "footer"}}
3+<hr />
4+{{end}}
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}}
+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}}
+0,
-0
+0,
-0
+0,
-0
+0,
-0
+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+}
+2,
-0
1@@ -0,0 +1,2 @@
2+User-agent: *
3+Allow: /
+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 {