A community based topic aggregation platform built on atproto
1package oauth 2 3import ( 4 "Coves/internal/atproto/identity" 5 "Coves/internal/atproto/oauth" 6 "encoding/json" 7 "log" 8 "net/http" 9 "net/url" 10 "strings" 11 12 oauthCore "Coves/internal/core/oauth" 13) 14 15// LoginHandler handles OAuth login flow initiation 16type LoginHandler struct { 17 identityResolver identity.Resolver 18 sessionStore oauthCore.SessionStore 19} 20 21// NewLoginHandler creates a new login handler 22func NewLoginHandler(identityResolver identity.Resolver, sessionStore oauthCore.SessionStore) *LoginHandler { 23 return &LoginHandler{ 24 identityResolver: identityResolver, 25 sessionStore: sessionStore, 26 } 27} 28 29// HandleLogin initiates the OAuth login flow 30// POST /oauth/login 31// Body: { "handle": "alice.bsky.social" } 32func (h *LoginHandler) HandleLogin(w http.ResponseWriter, r *http.Request) { 33 if r.Method != http.MethodPost { 34 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 35 return 36 } 37 38 // Parse request body 39 var req struct { 40 Handle string `json:"handle"` 41 ReturnURL string `json:"returnUrl,omitempty"` 42 } 43 44 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 45 http.Error(w, "Invalid request body", http.StatusBadRequest) 46 return 47 } 48 49 // Normalize handle 50 handle := strings.TrimSpace(strings.ToLower(req.Handle)) 51 handle = strings.TrimPrefix(handle, "@") 52 53 // Validate handle format 54 if handle == "" || !strings.Contains(handle, ".") { 55 http.Error(w, "Invalid handle format", http.StatusBadRequest) 56 return 57 } 58 59 // Resolve handle to DID and PDS 60 resolved, err := h.identityResolver.Resolve(r.Context(), handle) 61 if err != nil { 62 log.Printf("Failed to resolve handle %s: %v", handle, err) 63 http.Error(w, "Unable to find that account", http.StatusBadRequest) 64 return 65 } 66 67 // Get OAuth client configuration (supports base64 encoding) 68 privateJWK, err := GetEnvBase64OrPlain("OAUTH_PRIVATE_JWK") 69 if err != nil { 70 log.Printf("Failed to load OAuth private key: %v", err) 71 http.Error(w, "OAuth configuration error", http.StatusInternalServerError) 72 return 73 } 74 if privateJWK == "" { 75 http.Error(w, "OAuth not configured", http.StatusInternalServerError) 76 return 77 } 78 79 privateKey, err := oauth.ParseJWKFromJSON([]byte(privateJWK)) 80 if err != nil { 81 log.Printf("Failed to parse OAuth private key: %v", err) 82 http.Error(w, "OAuth configuration error", http.StatusInternalServerError) 83 return 84 } 85 86 appviewURL := getAppViewURL() 87 clientID := getClientID(appviewURL) 88 redirectURI := appviewURL + "/oauth/callback" 89 90 // Create OAuth client 91 client := oauth.NewClient(clientID, privateKey, redirectURI) 92 93 // Discover auth server from PDS 94 pdsURL := resolved.PDSURL 95 authServerIss, err := client.ResolvePDSAuthServer(r.Context(), pdsURL) 96 if err != nil { 97 log.Printf("Failed to resolve auth server for PDS %s: %v", pdsURL, err) 98 http.Error(w, "Failed to discover authorization server", http.StatusInternalServerError) 99 return 100 } 101 102 // Fetch auth server metadata 103 authMeta, err := client.FetchAuthServerMetadata(r.Context(), authServerIss) 104 if err != nil { 105 log.Printf("Failed to fetch auth server metadata: %v", err) 106 http.Error(w, "Failed to fetch authorization server metadata", http.StatusInternalServerError) 107 return 108 } 109 110 // Generate DPoP key for this session 111 dpopKey, err := oauth.GenerateDPoPKey() 112 if err != nil { 113 log.Printf("Failed to generate DPoP key: %v", err) 114 http.Error(w, "Failed to generate session key", http.StatusInternalServerError) 115 return 116 } 117 118 // Send PAR request 119 parResp, err := client.SendPARRequest(r.Context(), authMeta, handle, "atproto transition:generic", dpopKey) 120 if err != nil { 121 log.Printf("Failed to send PAR request: %v", err) 122 http.Error(w, "Failed to initiate authorization", http.StatusInternalServerError) 123 return 124 } 125 126 // Serialize DPoP key to JSON 127 dpopKeyJSON, err := oauth.JWKToJSON(dpopKey) 128 if err != nil { 129 log.Printf("Failed to serialize DPoP key: %v", err) 130 http.Error(w, "Failed to store session key", http.StatusInternalServerError) 131 return 132 } 133 134 // Save OAuth request state to database 135 oauthReq := &oauthCore.OAuthRequest{ 136 State: parResp.State, 137 DID: resolved.DID, 138 Handle: handle, 139 PDSURL: pdsURL, 140 PKCEVerifier: parResp.PKCEVerifier, 141 DPoPPrivateJWK: string(dpopKeyJSON), 142 DPoPAuthServerNonce: parResp.DpopAuthserverNonce, 143 AuthServerIss: authServerIss, 144 ReturnURL: req.ReturnURL, 145 } 146 147 if saveErr := h.sessionStore.SaveRequest(oauthReq); saveErr != nil { 148 log.Printf("Failed to save OAuth request: %v", saveErr) 149 http.Error(w, "Failed to save authorization state", http.StatusInternalServerError) 150 return 151 } 152 153 // Build authorization URL 154 authURL, err := url.Parse(authMeta.AuthorizationEndpoint) 155 if err != nil { 156 log.Printf("Invalid authorization endpoint: %v", err) 157 http.Error(w, "Invalid authorization endpoint", http.StatusInternalServerError) 158 return 159 } 160 161 query := authURL.Query() 162 query.Set("client_id", clientID) 163 query.Set("request_uri", parResp.RequestURI) 164 authURL.RawQuery = query.Encode() 165 166 // Return authorization URL to client 167 resp := map[string]string{ 168 "authorizationUrl": authURL.String(), 169 "state": parResp.State, 170 } 171 172 w.Header().Set("Content-Type", "application/json") 173 w.WriteHeader(http.StatusOK) 174 if err := json.NewEncoder(w).Encode(resp); err != nil { 175 log.Printf("Failed to encode response: %v", err) 176 } 177}