Live video on the AT Protocol
79
fork

Configure Feed

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

at eli/get-segments-rebased 234 lines 6.8 kB view raw
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 w.WriteHeader(http.StatusInternalServerError) 67 w.Write([]byte(err.Error())) 68 return 69 } 70 if session == nil { 71 next.ServeHTTP(w, r) 72 return 73 } 74 ctx = context.WithValue(ctx, OAuthSessionContextKey, session) 75 ctx = context.WithValue(ctx, OProxyContextKey, o) 76 next.ServeHTTP(w, r.WithContext(ctx)) 77 }) 78} 79 80func getMethod(method string) (dpop.HTTPVerb, error) { 81 switch method { 82 case "POST": 83 return dpop.POST, nil 84 case "GET": 85 return dpop.GET, nil 86 } 87 return "", fmt.Errorf("invalid method") 88} 89 90func (o *OProxy) getOAuthSession(r *http.Request, w http.ResponseWriter) (*OAuthSession, error) { 91 92 authHeader := r.Header.Get("Authorization") 93 if authHeader == "" { 94 return nil, nil 95 } 96 if !strings.HasPrefix(authHeader, "DPoP ") { 97 return nil, fmt.Errorf("invalid authorization header (must start with DPoP)") 98 } 99 token := strings.TrimPrefix(authHeader, "DPoP ") 100 101 dpopHeader := r.Header.Get("DPoP") 102 if dpopHeader == "" { 103 return nil, fmt.Errorf("missing DPoP header") 104 } 105 106 dpopMethod, err := getMethod(r.Method) 107 if err != nil { 108 return nil, fmt.Errorf("invalid method: %w", err) 109 } 110 111 u, err := url.Parse(r.URL.String()) 112 if err != nil { 113 return nil, fmt.Errorf("invalid url: %w", err) 114 } 115 u.Scheme = "https" 116 u.Host = r.Host 117 u.RawQuery = "" 118 u.Fragment = "" 119 120 jkt, nonce, err := getJKT(dpopHeader) 121 122 session, err := o.loadOAuthSession(jkt) 123 if err != nil { 124 return nil, fmt.Errorf("could not get oauth session: %w", err) 125 } 126 if session == nil { 127 return nil, fmt.Errorf("oauth session not found") 128 } 129 if session.RevokedAt != nil { 130 return nil, fmt.Errorf("oauth session revoked") 131 } 132 if session.DownstreamDPoPNonce != nonce { 133 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"`) 134 w.Header().Set("DPoP-Nonce", session.DownstreamDPoPNonce) 135 return nil, dpop.ErrIncorrectNonce 136 } 137 138 session.DownstreamDPoPNonce = makeNonce() 139 err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 140 if err != nil { 141 return nil, fmt.Errorf("could not update downstream session: %w", err) 142 } 143 w.Header().Set("DPoP-Nonce", session.DownstreamDPoPNonce) 144 145 proof, err := dpop.Parse(dpopHeader, dpopMethod, u, dpop.ParseOptions{ 146 Nonce: nonce, 147 TimeWindow: &dpopTimeWindow, 148 }) 149 // Check the error type to determine response 150 if err != nil { 151 if ok := errors.Is(err, dpop.ErrInvalidProof); ok { 152 // Return 'invalid_dpop_proof' 153 return nil, fmt.Errorf("invalid DPoP proof: %w", err) 154 } 155 return nil, fmt.Errorf("error validating proof proof: %w", err) 156 } 157 158 // Hash the token with base64 and SHA256 159 // Get the access token JWT (introspect if needed) 160 // Parse the access token JWT and verify the signature 161 // Hash the access token with SHA-256 162 hasher := sha256.New() 163 hasher.Write([]byte(token)) 164 hash := hasher.Sum(nil) 165 166 // Encode the hash in URL-safe base64 format without padding 167 // accessTokenHash := base64.RawURLEncoding.EncodeToString(hash) 168 accessTokenHash := base64.RawURLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash) 169 pubKey, err := o.downstreamJWK.PublicKey() 170 if err != nil { 171 return nil, fmt.Errorf("could not get access jwk public key: %w", err) 172 } 173 var pubKeyECDSA ecdsa.PublicKey 174 err = pubKey.Raw(&pubKeyECDSA) 175 if err != nil { 176 return nil, fmt.Errorf("could not get access jwk public key: %w", err) 177 } 178 179 // Parse the access token JWT 180 claims := &dpop.BoundAccessTokenClaims{} 181 accessTokenJWT, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (any, error) { 182 return &pubKeyECDSA, nil 183 }) 184 185 if err != nil { 186 return nil, fmt.Errorf("could not parse access token: %w", err) 187 } 188 189 err = proof.Validate([]byte(accessTokenHash), accessTokenJWT) 190 // Check the error type to determine response 191 if err != nil { 192 return nil, fmt.Errorf("invalid proof: %w", err) 193 } 194 195 return session, nil 196} 197 198func (o *OProxy) DPoPNonceMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 199 return func(c echo.Context) error { 200 dpopHeader := c.Request().Header.Get("DPoP") 201 if dpopHeader == "" { 202 return echo.NewHTTPError(http.StatusBadRequest, "missing DPoP header") 203 } 204 205 jkt, _, err := getJKT(dpopHeader) 206 if err != nil { 207 return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 208 } 209 210 session, err := o.loadOAuthSession(jkt) 211 if err != nil { 212 return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 213 } 214 215 c.Set("session", session) 216 return next(c) 217 } 218} 219 220func (o *OProxy) ErrorHandlingMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 221 return func(c echo.Context) error { 222 err := next(c) 223 if err == nil { 224 return nil 225 } 226 httpError, ok := err.(*echo.HTTPError) 227 if ok { 228 o.slog.Error("oauth error", "code", httpError.Code, "message", httpError.Message, "internal", httpError.Internal) 229 return err 230 } 231 o.slog.Error("unhandled error", "error", err) 232 return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 233 } 234}