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.
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 89 lines 2.8 kB view raw
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}