A community based topic aggregation platform built on atproto
1package oauth
2
3import (
4 "encoding/json"
5 "log"
6 "net/http"
7 "net/url"
8 "strings"
9
10 "Coves/internal/atproto/identity"
11 "Coves/internal/atproto/oauth"
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 err := h.sessionStore.SaveRequest(oauthReq); err != nil {
148 log.Printf("Failed to save OAuth request: %v", err)
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 json.NewEncoder(w).Encode(resp)
175}