A locally focused bluesky appview
at master 3.1 kB view raw
1package xrpc 2 3import ( 4 "context" 5 "fmt" 6 "net/http" 7 "strings" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/labstack/echo/v4" 11 "github.com/lestrrat-go/jwx/v2/jwt" 12) 13 14// requireAuth is middleware that requires authentication 15func (s *Server) requireAuth(next echo.HandlerFunc) echo.HandlerFunc { 16 return func(c echo.Context) error { 17 viewer, err := s.authenticate(c) 18 if err != nil { 19 return XRPCError(c, http.StatusUnauthorized, "AuthenticationRequired", err.Error()) 20 } 21 c.Set("viewer", viewer) 22 return next(c) 23 } 24} 25 26// optionalAuth is middleware that optionally authenticates 27func (s *Server) optionalAuth(next echo.HandlerFunc) echo.HandlerFunc { 28 return func(c echo.Context) error { 29 viewer, _ := s.authenticate(c) 30 if viewer != "" { 31 c.Set("viewer", viewer) 32 } 33 return next(c) 34 } 35} 36 37// authenticate extracts and validates the JWT from the Authorization header 38// Returns the viewer DID if valid, empty string otherwise 39func (s *Server) authenticate(c echo.Context) (string, error) { 40 authHeader := c.Request().Header.Get("Authorization") 41 if authHeader == "" { 42 return "", fmt.Errorf("missing authorization header") 43 } 44 45 // Extract Bearer token 46 parts := strings.Split(authHeader, " ") 47 if len(parts) != 2 || parts[0] != "Bearer" { 48 return "", fmt.Errorf("invalid authorization header format") 49 } 50 51 tokenString := parts[1] 52 53 // Parse JWT without signature validation (for development) 54 // In production, you'd want to validate the signature using the issuer's public key 55 token, err := jwt.Parse([]byte(tokenString), jwt.WithVerify(false), jwt.WithValidate(false)) 56 if err != nil { 57 return "", fmt.Errorf("failed to parse token: %w", err) 58 } 59 60 // Extract the user's DID - try both "sub" (PDS tokens) and "iss" (service tokens) 61 var userDID string 62 63 // First try "sub" claim (used by PDS tokens and entryway tokens) 64 sub := token.Subject() 65 if sub != "" && strings.HasPrefix(sub, "did:") { 66 userDID = sub 67 } else { 68 // Fall back to "iss" claim (used by some service tokens) 69 iss := token.Issuer() 70 if iss != "" && strings.HasPrefix(iss, "did:") { 71 userDID = iss 72 } 73 } 74 75 if userDID == "" { 76 return "", fmt.Errorf("missing 'sub' or 'iss' claim with DID in token") 77 } 78 79 // Optional: check scope if present 80 scope, ok := token.Get("scope") 81 if ok { 82 scopeStr, _ := scope.(string) 83 // Valid scopes are: com.atproto.access, com.atproto.appPass, com.atproto.appPassPrivileged 84 if scopeStr != "com.atproto.access" && scopeStr != "com.atproto.appPass" && scopeStr != "com.atproto.appPassPrivileged" { 85 return "", fmt.Errorf("invalid token scope: %s", scopeStr) 86 } 87 } 88 89 return userDID, nil 90} 91 92// resolveActor resolves an actor identifier (handle or DID) to a DID 93func (s *Server) resolveActor(ctx context.Context, actor string) (string, error) { 94 // If it's already a DID, return it 95 if strings.HasPrefix(actor, "did:") { 96 return actor, nil 97 } 98 99 // Otherwise, resolve the handle 100 resp, err := s.dir.LookupHandle(ctx, syntax.Handle(actor)) 101 if err != nil { 102 return "", fmt.Errorf("failed to resolve handle: %w", err) 103 } 104 105 return resp.DID.String(), nil 106}