this repo has no description
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7 "net/url"
8 "strings"
9 "time"
10
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 "github.com/gorilla/sessions"
13 oauth_helpers "github.com/haileyok/atproto-oauth-golang/helpers"
14 "github.com/labstack/echo-contrib/session"
15 "github.com/labstack/echo/v4"
16 "gorm.io/gorm/clause"
17)
18
19func (s *TestServer) handleLoginSubmit(e echo.Context) error {
20 authInput := strings.ToLower(e.FormValue("auth-input"))
21 if authInput == "" {
22 return e.Redirect(302, "/login?e=auth-input-empty")
23 }
24
25 var service string
26 var did string
27 var loginHint string
28
29 if strings.HasPrefix("https://", authInput) {
30 u, err := url.Parse(authInput)
31 if err == nil {
32 u.Path = ""
33 u.RawQuery = ""
34 u.User = nil
35 service = u.String()
36 }
37 } else {
38 _, herr := syntax.ParseHandle(authInput)
39 _, derr := syntax.ParseDID(authInput)
40
41 if herr != nil && derr != nil {
42 return e.Redirect(302, "/login?e=handle-invalid")
43 }
44
45 if derr == nil {
46 did = authInput
47 } else {
48 maybeDid, err := resolveHandle(e.Request().Context(), authInput)
49 if err != nil {
50 return err
51 }
52
53 did = maybeDid
54 }
55
56 maybeService, err := resolveService(ctx, did)
57 if err != nil {
58 return err
59 }
60
61 service = maybeService
62 loginHint = authInput
63 }
64
65 authserver, err := s.oauthClient.ResolvePdsAuthServer(ctx, service)
66 if err != nil {
67 return err
68 }
69
70 meta, err := s.oauthClient.FetchAuthServerMetadata(ctx, authserver)
71 if err != nil {
72 return err
73 }
74
75 dpopPrivateKey, err := oauth_helpers.GenerateKey(nil)
76 if err != nil {
77 return err
78 }
79
80 dpopPrivateKeyJson, err := json.Marshal(dpopPrivateKey)
81 if err != nil {
82 return err
83 }
84
85 parResp, err := s.oauthClient.SendParAuthRequest(ctx, authserver, meta, loginHint, scope, dpopPrivateKey)
86 if err != nil {
87 return err
88 }
89
90 oauthRequest := &OauthRequest{
91 State: parResp.State,
92 AuthserverIss: meta.Issuer,
93 Did: did,
94 PdsUrl: service,
95 PkceVerifier: parResp.PkceVerifier,
96 DpopAuthserverNonce: parResp.DpopAuthserverNonce,
97 DpopPrivateJwk: string(dpopPrivateKeyJson),
98 }
99
100 if err := s.db.Create(oauthRequest).Error; err != nil {
101 return err
102 }
103
104 params := url.Values{
105 "client_id": {s.args.UrlRoot + serverMetadataPath},
106 "request_uri": {parResp.RequestUri},
107 }
108
109 u, _ := url.Parse(meta.AuthorizationEndpoint)
110 u.RawQuery = params.Encode()
111
112 sess, err := session.Get("session", e)
113 if err != nil {
114 return err
115 }
116
117 sess.Options = &sessions.Options{
118 Path: "/",
119 MaxAge: 300, // save for five minutes
120 HttpOnly: true,
121 }
122
123 // make sure the session is empty
124 sess.Values = map[any]any{}
125 sess.Values["oauth_state"] = parResp.State
126 sess.Values["oauth_did"] = did
127
128 if err := sess.Save(e.Request(), e.Response()); err != nil {
129 return err
130 }
131
132 return e.Redirect(302, u.String())
133}
134
135func (s *TestServer) handleCallback(e echo.Context) error {
136 params := e.QueryParams()
137 state := params.Get("state")
138 iss := params.Get("iss")
139 code := params.Get("code")
140
141 sess, err := session.Get("session", e)
142 if err != nil {
143 return err
144 }
145
146 sessState := sess.Values["oauth_state"]
147
148 if state == "" || iss == "" || code == "" {
149 return fmt.Errorf("request missing needed parameters")
150 }
151
152 if state != sessState {
153 return fmt.Errorf("session state does not match response state")
154 }
155
156 var oauthRequest OauthRequest
157 if err := s.db.Raw("SELECT * FROM oauth_requests WHERE state = ?", sessState).Scan(&oauthRequest).Error; err != nil {
158 return err
159 }
160
161 if err := s.db.Exec("DELETE FROM oauth_requests WHERE state = ?", sessState).Error; err != nil {
162 return err
163 }
164
165 if iss != oauthRequest.AuthserverIss {
166 return fmt.Errorf("incoming iss did not match authserver iss")
167 }
168
169 jwk, err := oauth_helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk))
170 if err != nil {
171 return err
172 }
173
174 initialTokenResp, err := s.oauthClient.InitialTokenRequest(e.Request().Context(), code, iss, oauthRequest.PkceVerifier, oauthRequest.DpopAuthserverNonce, jwk)
175 if err != nil {
176 return err
177 }
178
179 if initialTokenResp.Scope != scope {
180 return fmt.Errorf("did not receive correct scopes from token request")
181 }
182
183 // if we didn't start with a did, we can get it from the response
184 if oauthRequest.Did == "" {
185 oauthRequest.Did = initialTokenResp.Sub
186 }
187
188 oauthSession := &OauthSession{
189 Did: oauthRequest.Did,
190 PdsUrl: oauthRequest.PdsUrl,
191 AuthserverIss: oauthRequest.AuthserverIss,
192 AccessToken: initialTokenResp.AccessToken,
193 RefreshToken: initialTokenResp.RefreshToken,
194 DpopAuthserverNonce: initialTokenResp.DpopAuthserverNonce,
195 DpopPrivateJwk: oauthRequest.DpopPrivateJwk,
196 Expiration: time.Now().Add(time.Duration(int(time.Second) * int(initialTokenResp.ExpiresIn))),
197 }
198
199 if err := s.db.Clauses(clause.OnConflict{
200 Columns: []clause.Column{{Name: "did"}},
201 UpdateAll: true,
202 }).Create(oauthSession).Error; err != nil {
203 return err
204 }
205
206 sess.Options = &sessions.Options{
207 Path: "/",
208 MaxAge: 86400 * 7,
209 HttpOnly: true,
210 }
211
212 // make sure the session is empty
213 sess.Values = map[any]any{}
214 sess.Values["did"] = oauthRequest.Did
215
216 if err := sess.Save(e.Request(), e.Response()); err != nil {
217 return err
218 }
219
220 slog.Default().Info("handled callback", "params", params)
221
222 return e.Redirect(302, "/")
223}
224
225func (s *TestServer) handleLogout(e echo.Context) error {
226 sess, err := session.Get("session", e)
227 if err != nil {
228 return err
229 }
230
231 sess.Options = &sessions.Options{
232 Path: "/",
233 MaxAge: -1,
234 HttpOnly: true,
235 }
236
237 if err := sess.Save(e.Request(), e.Response()); err != nil {
238 return err
239 }
240
241 return e.Redirect(302, "/")
242}