mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-26 20:24:16 +00:00
102 lines
3.3 KiB
Go
102 lines
3.3 KiB
Go
package atproto
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
mainAtproto "atcr.io/pkg/atproto"
|
|
)
|
|
|
|
// TokenValidator validates ATProto OAuth access tokens
|
|
type TokenValidator struct {
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewTokenValidator creates a new token validator
|
|
func NewTokenValidator() *TokenValidator {
|
|
return &TokenValidator{
|
|
httpClient: &http.Client{},
|
|
}
|
|
}
|
|
|
|
// SessionInfo represents the response from com.atproto.server.getSession
|
|
type SessionInfo struct {
|
|
DID string `json:"did"`
|
|
Handle string `json:"handle"`
|
|
Email string `json:"email,omitempty"`
|
|
EmailConfirmed bool `json:"emailConfirmed,omitempty"`
|
|
Active bool `json:"active,omitempty"`
|
|
}
|
|
|
|
// ValidateToken validates an ATProto OAuth access token by calling getSession
|
|
// Returns the user's DID and handle if the token is valid
|
|
// dpopProof is optional - if provided, uses DPoP auth; otherwise uses Bearer
|
|
func (v *TokenValidator) ValidateToken(ctx context.Context, pdsEndpoint, accessToken, dpopProof string) (*SessionInfo, error) {
|
|
// Call com.atproto.server.getSession with the access token
|
|
url := fmt.Sprintf("%s/xrpc/com.atproto.server.getSession", pdsEndpoint)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Always use Bearer auth for getSession validation
|
|
// The DPoP proof from the client is bound to their request to us (POST /auth/exchange),
|
|
// not to our request to the PDS (GET /getSession)
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
|
|
fmt.Printf("DEBUG [validator]: calling %s with Bearer auth, token_prefix=%s...\n",
|
|
url, accessToken[:min(20, len(accessToken))])
|
|
|
|
resp, err := v.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get session: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read body once for both logging and error handling
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
fmt.Printf("DEBUG [validator]: getSession returned 401: %s\n", string(bodyBytes))
|
|
return nil, fmt.Errorf("invalid or expired token")
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
fmt.Printf("DEBUG [validator]: getSession failed with status %d: %s\n", resp.StatusCode, string(bodyBytes))
|
|
return nil, fmt.Errorf("getSession failed with status %d: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var session SessionInfo
|
|
if err := json.Unmarshal(bodyBytes, &session); err != nil {
|
|
return nil, fmt.Errorf("failed to decode session: %w", err)
|
|
}
|
|
|
|
// Validate required fields
|
|
if session.DID == "" {
|
|
return nil, fmt.Errorf("session response missing DID")
|
|
}
|
|
if session.Handle == "" {
|
|
return nil, fmt.Errorf("session response missing handle")
|
|
}
|
|
|
|
return &session, nil
|
|
}
|
|
|
|
// ValidateTokenWithResolver validates a token and automatically resolves the PDS endpoint
|
|
// dpopProof is optional - if provided, uses DPoP auth; otherwise uses Bearer
|
|
func (v *TokenValidator) ValidateTokenWithResolver(ctx context.Context, handle, accessToken, dpopProof string) (*SessionInfo, error) {
|
|
// Resolve handle to PDS endpoint
|
|
resolver := mainAtproto.NewResolver()
|
|
_, pdsEndpoint, err := resolver.ResolveIdentity(ctx, handle)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to resolve PDS endpoint: %w", err)
|
|
}
|
|
|
|
// Validate token against the PDS
|
|
return v.ValidateToken(ctx, pdsEndpoint, accessToken, dpopProof)
|
|
}
|