Live video on the AT Protocol
79
fork

Configure Feed

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

at natb/home-menu 178 lines 6.2 kB view raw
1package oproxy 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 "net/url" 10 "slices" 11 12 "github.com/AxisCommunications/go-dpop" 13 "github.com/labstack/echo/v4" 14 "go.opentelemetry.io/otel" 15) 16 17type PAR struct { 18 ClientID string `json:"client_id"` 19 RedirectURI string `json:"redirect_uri"` 20 CodeChallenge string `json:"code_challenge"` 21 CodeChallengeMethod string `json:"code_challenge_method"` 22 State string `json:"state"` 23 LoginHint string `json:"login_hint"` 24 ResponseMode string `json:"response_mode"` 25 ResponseType string `json:"response_type"` 26 Scope string `json:"scope"` 27} 28 29type PARResponse struct { 30 RequestURI string `json:"request_uri"` 31 ExpiresIn int `json:"expires_in"` 32} 33 34var ErrFirstNonce = echo.NewHTTPError(http.StatusBadRequest, "first time seeing this key, come back with a nonce") 35 36func (o *OProxy) HandleOAuthPAR(c echo.Context) error { 37 ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthPAR") 38 defer span.End() 39 c.Response().Header().Set("Access-Control-Allow-Origin", "*") 40 var par PAR 41 if err := json.NewDecoder(c.Request().Body).Decode(&par); err != nil { 42 return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 43 } 44 45 dpopHeader := c.Request().Header.Get("DPoP") 46 if dpopHeader == "" { 47 return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required") 48 } 49 50 resp, err := o.NewPAR(ctx, c, &par, dpopHeader) 51 if errors.Is(err, ErrFirstNonce) { 52 res := map[string]interface{}{ 53 "error": "use_dpop_nonce", 54 "error_description": "Authorization server requires nonce in DPoP proof", 55 } 56 return c.JSON(http.StatusBadRequest, res) 57 } else if err != nil { 58 return err 59 } 60 return c.JSON(http.StatusCreated, resp) 61} 62 63func (o *OProxy) NewPAR(ctx context.Context, c echo.Context, par *PAR, dpopHeader string) (*PARResponse, error) { 64 jkt, nonce, err := getJKT(dpopHeader) 65 if err != nil { 66 return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get JKT from DPoP header header=%s: %s", dpopHeader, err)) 67 } 68 session, err := o.loadOAuthSession(jkt) 69 if err != nil { 70 return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to load OAuth session: %s", err)) 71 } 72 // special case - if this is the first request, we need to send it back for a new nonce 73 if session == nil { 74 _, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{ 75 Nonce: nonce, // normally this would be bad! but on the first request we're revalidating nonce anyway 76 TimeWindow: &dpopTimeWindow, 77 }) 78 if err != nil { 79 return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse DPoP header: %s", err)) 80 } 81 newNonce := makeNonce() 82 err = o.createOAuthSession(jkt, &OAuthSession{ 83 DownstreamDPoPJKT: jkt, 84 DownstreamDPoPNonce: newNonce, 85 }) 86 if err != nil { 87 return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth session: %s", err)) 88 } 89 // come back later, nerd 90 c.Response().Header().Set("DPoP-Nonce", newNonce) 91 return nil, ErrFirstNonce 92 } 93 if session.DownstreamDPoPNonce != nonce { 94 return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid nonce") 95 } 96 proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{ 97 Nonce: session.DownstreamDPoPNonce, 98 TimeWindow: &dpopTimeWindow, 99 }) 100 // Check the error type to determine response 101 if err != nil { 102 // if ok := errors.Is(err, dpop.ErrInvalidProof); ok { 103 // apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", nil) 104 // return 105 // } 106 // apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", err) 107 // return 108 return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid DPoP proof: %s", err)) 109 } 110 if proof.PublicKey() != jkt { 111 panic("invalid code path: parsed DPoP proof twice and got different keys?!") 112 } 113 114 clientMetadata, err := o.GetDownstreamMetadata(par.RedirectURI) 115 if err != nil { 116 return nil, err 117 } 118 if par.ClientID != clientMetadata.ClientID { 119 return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid client_id: expected %s, got %s", clientMetadata.ClientID, par.ClientID)) 120 } 121 122 if !slices.Contains(clientMetadata.RedirectURIs, par.RedirectURI) { 123 return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", par.RedirectURI)) 124 } 125 126 if par.CodeChallengeMethod != "S256" { 127 return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid code challenge method: expected S256, got %s", par.CodeChallengeMethod)) 128 } 129 130 if par.ResponseMode != "query" { 131 return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response mode: expected query, got %s", par.ResponseMode)) 132 } 133 134 if par.ResponseType != "code" { 135 return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response type: expected code, got %s", par.ResponseType)) 136 } 137 138 if par.Scope != o.scope { 139 return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid scope") 140 } 141 142 if par.LoginHint == "" { 143 return nil, echo.NewHTTPError(http.StatusBadRequest, "login hint is required to find your PDS") 144 } 145 146 if par.State == "" { 147 return nil, echo.NewHTTPError(http.StatusBadRequest, "state is required") 148 } 149 150 if par.Scope != o.scope { 151 return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid scope (expected %s, got %s)", o.scope, par.Scope)) 152 } 153 154 urn := makeURN(jkt) 155 156 newNonce := makeNonce() 157 158 err = o.updateOAuthSession(jkt, &OAuthSession{ 159 DownstreamDPoPJKT: jkt, 160 DownstreamDPoPNonce: newNonce, 161 DownstreamPARRequestURI: urn, 162 DownstreamCodeChallenge: par.CodeChallenge, 163 DownstreamState: par.State, 164 DownstreamRedirectURI: par.RedirectURI, 165 Handle: par.LoginHint, 166 }) 167 if err != nil { 168 return nil, fmt.Errorf("could not create oauth session: %w", err) 169 } 170 c.Response().Header().Set("DPoP-Nonce", newNonce) 171 172 resp := &PARResponse{ 173 RequestURI: urn, 174 ExpiresIn: int(dpopTimeWindow.Seconds()), 175 } 176 177 return resp, nil 178}