Live video on the AT Protocol
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}