Stateless auth proxy that converts AT Protocol native apps from public to confidential OAuth clients. Deploy once, get 180-day refresh tokens instead of 24-hour ones.
1package main
2
3import (
4 "encoding/json"
5 "log"
6 "net/http"
7 "net/url"
8)
9
10type parRequest struct {
11 PAREndpoint string `json:"par_endpoint"`
12 Issuer string `json:"issuer"`
13 KeyID string `json:"key_id,omitempty"`
14 LoginHint string `json:"login_hint,omitempty"`
15 Scope string `json:"scope"`
16 CodeChallenge string `json:"code_challenge"`
17 CodeChallengeMethod string `json:"code_challenge_method"`
18 State string `json:"state"`
19 RedirectURI string `json:"redirect_uri"`
20}
21
22func HandlePAR(signers *SignerSet, clientID string) http.HandlerFunc {
23 return func(w http.ResponseWriter, r *http.Request) {
24 var req parRequest
25 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
26 writeJSONError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body")
27 return
28 }
29
30 if req.PAREndpoint == "" {
31 writeJSONError(w, http.StatusBadRequest, "invalid_request", "par_endpoint is required")
32 return
33 }
34 if req.Issuer == "" {
35 writeJSONError(w, http.StatusBadRequest, "invalid_request", "issuer is required")
36 return
37 }
38
39 if err := ValidatePAREndpointForIssuer(r.Context(), req.Issuer, req.PAREndpoint); err != nil {
40 writeAPIError(w, err)
41 return
42 }
43
44 candidateKeyIDs, err := signers.CandidateKeyIDs(req.KeyID)
45 if err != nil {
46 writeJSONError(w, http.StatusBadRequest, "invalid_request", err.Error())
47 return
48 }
49
50 signer, err := signers.Lookup(candidateKeyIDs[0])
51 if err != nil {
52 writeJSONError(w, http.StatusBadRequest, "invalid_request", err.Error())
53 return
54 }
55
56 assertion, err := GenerateClientAssertion(signer, clientID, req.Issuer)
57 if err != nil {
58 log.Printf("failed to generate client assertion: %v", err)
59 writeJSONError(w, http.StatusInternalServerError, "server_error", "failed to generate client assertion")
60 return
61 }
62
63 params := url.Values{}
64 params.Set("response_type", "code")
65 params.Set("client_id", clientID)
66 params.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
67 params.Set("client_assertion", assertion)
68 params.Set("scope", req.Scope)
69 params.Set("code_challenge", req.CodeChallenge)
70 params.Set("code_challenge_method", req.CodeChallengeMethod)
71 params.Set("state", req.State)
72 params.Set("redirect_uri", req.RedirectURI)
73 if req.LoginHint != "" {
74 params.Set("login_hint", req.LoginHint)
75 }
76
77 proxied, err := PostForm(r.Context(), req.PAREndpoint, params, r.Header.Get("DPoP"))
78 if err != nil {
79 log.Printf("proxy request failed: %v", err)
80 writeAPIError(w, upstreamRequestError("upstream request failed"))
81 return
82 }
83
84 w.Header().Set(authProxyKeyIDHeader, candidateKeyIDs[0])
85 if err := WriteProxiedResponse(w, proxied); err != nil {
86 log.Printf("failed to write proxied response: %v", err)
87 }
88 }
89}