// oauth_consent.go — the /authorize consent screen (K7; advisory §6). // // Before K7 the GET form asked for the raw API key without saying WHO asked // or WHERE the browser would be sent afterwards, and it validated nothing: // with CTX_OAUTH_DCR_MODE=open anyone could register a client with an // attacker-owned https callback and send a victim a genuine // /authorize link — the key went into a code for the attacker's // client. The consent screen closes the UI half of that: // // - the GET validates client_id (registered, active) and redirect_uri // (the same exact-match rule the POST enforces) BEFORE anything is // rendered; a failure is an error page WITHOUT a key field (OAuth 2.1 // §4.1.2.1: an invalid client or redirect is never redirected to and // never offered the credential prompt); // - the form shows the client's name (format and control runes dropped, // bidi-isolated), its client_id and the redirect ORIGIN (scheme + host, // path shortened) — the address the code goes to; // - a client that registered itself (DCR without an acting admin key) // carries an explicit warning, unless the redirect this request asks // for is an operator-vetted callback (§F-§6, see newConsentView); // - an explicit consent checkbox, checked SERVER-side at POST — a key // alone no longer issues a code. // // The page is server-rendered html/template (contextual escaping) under a // script-free CSP; there is no JavaScript on it at all. package handler import ( "html/template" "log/slog" "net/http" "net/url" "strconv" "strings" "unicode/utf8" "github.com/GottZ/ctx/internal/store" ) // consentFieldValue is the value the consent checkbox posts. The POST // accepts exactly this value — an absent or different value issues no code. const consentFieldValue = "yes" // consentCSP: no script source at all (the page has none), inline

ctx — Authorize MCP Access

An application is asking for access to ctx with your API key.

Application
{{.ClientName}}
Client ID
{{.ClientID}}
You will be sent back to
{{.RedirectOrigin}}{{.RedirectPath}}
{{if .NonASCIIHost}}
Warning: the return address contains non-ASCII characters and is shown escaped. It may imitate a different domain.
{{end}}{{if .WarnSelfRegistered}}
Warning: This client registered itself. Only continue if you started this flow from that application and recognise the address above.
{{end}}{{if .Error}}
{{.Error}}
{{end}}


The application receives an access token with all rights of this key — not the key itself.

`)) var authorizeErrorTmpl = template.Must(template.New("authorize-error").Parse(` ctx — Authorize

{{.Title}}

{{.Detail}}

No access was granted. Return to the application and start the connection again.

`)) // writeAuthorizeHTML sets the page headers shared by the consent and error // pages and executes the template. func writeAuthorizeHTML(w http.ResponseWriter, status int, tmpl *template.Template, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Security-Policy", consentCSP) w.Header().Set("Cache-Control", "no-store") w.Header().Set("Referrer-Policy", "no-referrer") w.WriteHeader(status) if err := tmpl.Execute(w, data); err != nil { slog.Error("oauth: render authorize page", "error", err) } } // writeAuthorizeError renders the key-less error page. func writeAuthorizeError(w http.ResponseWriter, rej *authorizeReject) { writeAuthorizeHTML(w, rej.status, authorizeErrorTmpl, struct{ Title, Detail string }{rej.title, rej.detail}) } // writeConsent renders the consent page (status 200 on GET; the POST // re-renders it with an error message and a 4xx status). func writeConsent(w http.ResponseWriter, status int, client *store.OAuthClient, redirect *url.URL, p authorizeParams, errMsg string) { writeAuthorizeHTML(w, status, consentTmpl, newConsentView(client, redirect, p, errMsg)) }