oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "encoding/base64"
5 "net/http"
6 "net/url"
7 "time"
8
9 "amadan.net/oficina/home/handoff"
10 "amadan.net/rastrillo/rastrillo/assertion"
11 "amadan.net/rastrillo/rastrillo/sessions"
12 json "github.com/go-json-experiment/json"
13)
14
15const assertionIntentCookie = "__Host-oficina-assertion-intent"
16
17type assertionIntent struct {
18 Consumer string `json:"consumer"`
19 Nonce string `json:"nonce"`
20 Expires int64 `json:"expires"`
21 MaxAge int `json:"max_age,omitzero"`
22 Subject string `json:"subject,omitzero"`
23}
24
25func (a *App) handleAuthorize(w http.ResponseWriter, r *http.Request) {
26 w.Header().Set("Cache-Control", "no-store")
27 q, err := url.ParseQuery(r.URL.RawQuery)
28 maxAge := 0
29 if values, present := q["max_age"]; present {
30 if len(values) != 1 || values[0] != "300" {
31 a.failAssertion(w, r, authorizeAge)
32 return
33 }
34 maxAge = 300
35 }
36 wantFields := 2
37 if maxAge != 0 {
38 wantFields++
39 }
40 if err != nil || len(q) != wantFields || len(q["consumer"]) != 1 || len(q["nonce"]) != 1 || !handoff.ValidNonce(q.Get("nonce")) || a.runtime.consumers[q.Get("consumer")] == "" {
41 a.failAssertion(w, r, authorizeRequest)
42 return
43 }
44 intent := assertionIntent{Consumer: q.Get("consumer"), Nonce: q.Get("nonce"), Expires: time.Now().Unix() + 600, MaxAge: maxAge}
45 if maxAge != 0 {
46 if session, ok := a.sessions.From(r); ok {
47 intent.Subject = session.Subject
48 }
49 }
50 b, err := json.Marshal(intent)
51 if err != nil {
52 a.failAssertion(w, r, authorizeEncode)
53 return
54 }
55 sealed, err := a.secrets.Sealer.Seal("assertion.intent", b)
56 if err != nil {
57 a.failAssertion(w, r, authorizeSeal)
58 return
59 }
60 http.SetCookie(w, &http.Cookie{Name: assertionIntentCookie, Value: base64.RawURLEncoding.EncodeToString(sealed), Path: "/", MaxAge: 600, Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode})
61 http.Redirect(w, r, "/authorize/resume", http.StatusSeeOther)
62}
63func (a *App) handleAssertion(w http.ResponseWriter, r *http.Request) {
64 w.Header().Set("Cache-Control", "no-store")
65 // no-referrer would turn the cross-origin form POST's Origin into null.
66 w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
67 if r.URL.RawQuery != "" {
68 a.failAssertion(w, r, assertionQuery)
69 return
70 }
71 now := time.Now()
72 intent, ok := a.readAssertionIntent(r, now)
73 if !ok {
74 a.failAssertion(w, r, assertionIntentInvalid)
75 return
76 }
77 target, ok := a.runtime.consumers[intent.Consumer]
78 if !ok {
79 a.failAssertion(w, r, assertionConsumer)
80 return
81 }
82 session, ok := sessions.Current(r)
83 if !ok {
84 a.failAssertion(w, r, assertionSession)
85 return
86 }
87 authTime := session.AuthTime
88 if authTime.IsZero() {
89 authTime = session.At
90 }
91 if intent.Subject != "" && intent.Subject != session.Subject {
92 a.failAssertion(w, r, assertionSubject)
93 return
94 }
95 if intent.MaxAge != 0 && (!sessions.Fresh(session, time.Duration(intent.MaxAge)*time.Second, now) || authTime.After(now)) {
96 http.Redirect(w, r, "/signin?return_to=%2Fauthorize%2Fresume", http.StatusSeeOther)
97 return
98 }
99 id, err := assertion.RandomID()
100 if err != nil {
101 a.failAssertion(w, r, assertionRandom)
102 return
103 }
104 token, err := assertion.Sign(a.runtime.signer, assertion.Claims{Version: 1, Issuer: a.origins.Credential, Audience: target, Subject: session.Subject, KeyID: a.runtime.keyID, ID: id, Nonce: intent.Nonce, IssuedAt: now.Unix(), ExpiresAt: now.Unix() + 120, AuthTime: authTime.Unix()})
105 if err != nil {
106 a.failAssertion(w, r, assertionSign)
107 return
108 }
109 http.SetCookie(w, &http.Cookie{Name: assertionIntentCookie, Value: "", Path: "/", MaxAge: -1, Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode})
110 a.render(w, r, http.StatusOK, "assertion.html", map[string]any{"Action": target + handoff.CallbackPath, "Token": token})
111}
112