fork
Configure Feed
Select the types of activity you want to include in your feed.
Live video on the AT Protocol
fork
Configure Feed
Select the types of activity you want to include in your feed.
1package oproxy
2
3import (
4 "context"
5 "crypto/ecdsa"
6 "crypto/sha256"
7 "encoding/base64"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "net/http"
12 "net/url"
13 "strings"
14
15 "github.com/AxisCommunications/go-dpop"
16 "github.com/golang-jwt/jwt/v5"
17 "github.com/labstack/echo/v4"
18)
19
20var OAuthSessionContextKey = oauthSessionContextKeyType{}
21
22type oauthSessionContextKeyType struct{}
23
24var OProxyContextKey = oproxyContextKeyType{}
25
26type oproxyContextKeyType struct{}
27
28func GetOAuthSession(ctx context.Context) (*OAuthSession, *XrpcClient) {
29 o, ok := ctx.Value(OProxyContextKey).(*OProxy)
30 if !ok {
31 return nil, nil
32 }
33 session, ok := ctx.Value(OAuthSessionContextKey).(*OAuthSession)
34 if !ok {
35 return nil, nil
36 }
37 client, err := o.GetXrpcClient(session)
38 if err != nil {
39 return nil, nil
40 }
41 return session, client
42}
43
44func (o *OProxy) OAuthMiddleware(next http.Handler) http.Handler {
45 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46 // todo: see what these were set to before it got to us.
47 w.Header().Set("Access-Control-Allow-Origin", "*") // todo: ehhhhhhhhhhhh
48 w.Header().Set("Access-Control-Allow-Headers", "Content-Type,DPoP")
49 w.Header().Set("Access-Control-Allow-Methods", "*")
50 w.Header().Set("Access-Control-Expose-Headers", "DPoP-Nonce")
51
52 ctx := r.Context()
53 session, err := o.getOAuthSession(r, w)
54 if err != nil {
55 if errors.Is(err, dpop.ErrIncorrectNonce) {
56 // w.Header().Set("WWW-Authenticate", `DPoP error="use_dpop_nonce", error_description="Invalid nonce"`)
57 w.Header().Set("content-type", "application/json")
58 w.WriteHeader(http.StatusUnauthorized)
59 bs, _ := json.Marshal(map[string]interface{}{
60 "error": "use_dpop_nonce",
61 "error_description": "Authorization server requires nonce in DPoP proof",
62 })
63 w.Write(bs)
64 return
65 }
66 o.slog.Error("oauth error", "error", err)
67 w.WriteHeader(http.StatusInternalServerError)
68 w.Write([]byte(err.Error()))
69 return
70 }
71 if session == nil {
72 next.ServeHTTP(w, r)
73 return
74 }
75 ctx = context.WithValue(ctx, OAuthSessionContextKey, session)
76 ctx = context.WithValue(ctx, OProxyContextKey, o)
77 next.ServeHTTP(w, r.WithContext(ctx))
78 })
79}
80
81func getMethod(method string) (dpop.HTTPVerb, error) {
82 switch method {
83 case "POST":
84 return dpop.POST, nil
85 case "GET":
86 return dpop.GET, nil
87 }
88 return "", fmt.Errorf("invalid method")
89}
90
91func (o *OProxy) getOAuthSession(r *http.Request, w http.ResponseWriter) (*OAuthSession, error) {
92
93 authHeader := r.Header.Get("Authorization")
94 if authHeader == "" {
95 return nil, nil
96 }
97 if !strings.HasPrefix(authHeader, "DPoP ") {
98 return nil, nil
99 }
100 token := strings.TrimPrefix(authHeader, "DPoP ")
101
102 dpopHeader := r.Header.Get("DPoP")
103 if dpopHeader == "" {
104 return nil, fmt.Errorf("missing DPoP header")
105 }
106
107 dpopMethod, err := getMethod(r.Method)
108 if err != nil {
109 return nil, fmt.Errorf("invalid method: %w", err)
110 }
111
112 u, err := url.Parse(r.URL.String())
113 if err != nil {
114 return nil, fmt.Errorf("invalid url: %w", err)
115 }
116 u.Scheme = "https"
117 u.Host = r.Host
118 u.RawQuery = ""
119 u.Fragment = ""
120
121 jkt, nonce, err := getJKT(dpopHeader)
122
123 session, err := o.loadOAuthSession(jkt)
124 if err != nil {
125 return nil, fmt.Errorf("could not get oauth session: %w", err)
126 }
127 if session == nil {
128 // this can happen for stuff like getFeedSkeleton where they've submitted oauth credentials
129 // but they're not actually for this server
130 return nil, nil
131 }
132 if session.RevokedAt != nil {
133 return nil, fmt.Errorf("oauth session revoked")
134 }
135 if session.DownstreamDPoPNonce != nonce {
136 w.Header().Set("WWW-Authenticate", `DPoP algs="RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES256K ES384 ES512", error="use_dpop_nonce", error_description="Authorization server requires nonce in DPoP proof"`)
137 w.Header().Set("DPoP-Nonce", session.DownstreamDPoPNonce)
138 return nil, dpop.ErrIncorrectNonce
139 }
140
141 session.DownstreamDPoPNonce = makeNonce()
142 err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
143 if err != nil {
144 return nil, fmt.Errorf("could not update downstream session: %w", err)
145 }
146 w.Header().Set("DPoP-Nonce", session.DownstreamDPoPNonce)
147
148 proof, err := dpop.Parse(dpopHeader, dpopMethod, u, dpop.ParseOptions{
149 Nonce: nonce,
150 TimeWindow: &dpopTimeWindow,
151 })
152 // Check the error type to determine response
153 if err != nil {
154 if ok := errors.Is(err, dpop.ErrInvalidProof); ok {
155 // Return 'invalid_dpop_proof'
156 return nil, fmt.Errorf("invalid DPoP proof: %w", err)
157 }
158 return nil, fmt.Errorf("error validating proof proof: %w", err)
159 }
160
161 // Hash the token with base64 and SHA256
162 // Get the access token JWT (introspect if needed)
163 // Parse the access token JWT and verify the signature
164 // Hash the access token with SHA-256
165 hasher := sha256.New()
166 hasher.Write([]byte(token))
167 hash := hasher.Sum(nil)
168
169 // Encode the hash in URL-safe base64 format without padding
170 // accessTokenHash := base64.RawURLEncoding.EncodeToString(hash)
171 accessTokenHash := base64.RawURLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash)
172 pubKey, err := o.downstreamJWK.PublicKey()
173 if err != nil {
174 return nil, fmt.Errorf("could not get access jwk public key: %w", err)
175 }
176 var pubKeyECDSA ecdsa.PublicKey
177 err = pubKey.Raw(&pubKeyECDSA)
178 if err != nil {
179 return nil, fmt.Errorf("could not get access jwk public key: %w", err)
180 }
181
182 // Parse the access token JWT
183 claims := &dpop.BoundAccessTokenClaims{}
184 accessTokenJWT, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (any, error) {
185 return &pubKeyECDSA, nil
186 })
187
188 if err != nil {
189 return nil, fmt.Errorf("could not parse access token: %w", err)
190 }
191
192 err = proof.Validate([]byte(accessTokenHash), accessTokenJWT)
193 // Check the error type to determine response
194 if err != nil {
195 return nil, fmt.Errorf("invalid proof: %w", err)
196 }
197
198 return session, nil
199}
200
201func (o *OProxy) DPoPNonceMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
202 return func(c echo.Context) error {
203 dpopHeader := c.Request().Header.Get("DPoP")
204 if dpopHeader == "" {
205 return echo.NewHTTPError(http.StatusBadRequest, "missing DPoP header")
206 }
207
208 jkt, _, err := getJKT(dpopHeader)
209 if err != nil {
210 return echo.NewHTTPError(http.StatusBadRequest, err.Error())
211 }
212
213 session, err := o.loadOAuthSession(jkt)
214 if err != nil {
215 return echo.NewHTTPError(http.StatusBadRequest, err.Error())
216 }
217
218 c.Set("session", session)
219 return next(c)
220 }
221}
222
223func (o *OProxy) ErrorHandlingMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
224 return func(c echo.Context) error {
225 err := next(c)
226 if err == nil {
227 return nil
228 }
229 httpError, ok := err.(*echo.HTTPError)
230 if ok {
231 o.slog.Error("oauth error", "code", httpError.Code, "message", httpError.Message, "internal", httpError.Internal)
232 return err
233 }
234 o.slog.Error("unhandled error", "error", err)
235 return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
236 }
237}