Live video on the AT Protocol
fork

Configure Feed

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

at eli/docker-linting 237 lines 6.9 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 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}