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 "ext-one": {"hello"},
108 "ext-two": {"world"},
109 }
110
111 u, _ := url.Parse(meta.AuthorizationEndpoint)
112 u.RawQuery = params.Encode()
113
114 sess, err := session.Get("session", e)
115 if err != nil {
116 return err
117 }
118
119 sess.Options = &sessions.Options{
120 Path: "/",
121 MaxAge: 300, // save for five minutes
122 HttpOnly: true,
123 }
124
125 // make sure the session is empty
126 sess.Values = map[any]any{}
127 sess.Values["oauth_state"] = parResp.State
128 sess.Values["oauth_did"] = did
129
130 if err := sess.Save(e.Request(), e.Response()); err != nil {
131 return err
132 }
133
134 return e.Redirect(302, u.String())
135}
136
137func (s *TestServer) handleCallback(e echo.Context) error {
138 params := e.QueryParams()
139 state := params.Get("state")
140 iss := params.Get("iss")
141 code := params.Get("code")
142
143 sess, err := session.Get("session", e)
144 if err != nil {
145 return err
146 }
147
148 sessState := sess.Values["oauth_state"]
149
150 if state == "" || iss == "" || code == "" {
151 return fmt.Errorf("request missing needed parameters")
152 }
153
154 if state != sessState {
155 return fmt.Errorf("session state does not match response state")
156 }
157
158 var oauthRequest OauthRequest
159 if err := s.db.Raw("SELECT * FROM oauth_requests WHERE state = ?", sessState).Scan(&oauthRequest).Error; err != nil {
160 return err
161 }
162
163 if err := s.db.Exec("DELETE FROM oauth_requests WHERE state = ?", sessState).Error; err != nil {
164 return err
165 }
166
167 if iss != oauthRequest.AuthserverIss {
168 return fmt.Errorf("incoming iss did not match authserver iss")
169 }
170
171 jwk, err := oauth_helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk))
172 if err != nil {
173 return err
174 }
175
176 initialTokenResp, err := s.oauthClient.InitialTokenRequest(e.Request().Context(), code, iss, oauthRequest.PkceVerifier, oauthRequest.DpopAuthserverNonce, jwk)
177 if err != nil {
178 return err
179 }
180
181 if initialTokenResp.Scope != scope {
182 return fmt.Errorf("did not receive correct scopes from token request")
183 }
184
185 // if we didn't start with a did, we can get it from the response
186 if oauthRequest.Did == "" {
187 oauthRequest.Did = initialTokenResp.Sub
188 }
189
190 oauthSession := &OauthSession{
191 Did: oauthRequest.Did,
192 PdsUrl: oauthRequest.PdsUrl,
193 AuthserverIss: oauthRequest.AuthserverIss,
194 AccessToken: initialTokenResp.AccessToken,
195 RefreshToken: initialTokenResp.RefreshToken,
196 DpopAuthserverNonce: initialTokenResp.DpopAuthserverNonce,
197 DpopPrivateJwk: oauthRequest.DpopPrivateJwk,
198 Expiration: time.Now().Add(time.Duration(int(time.Second) * int(initialTokenResp.ExpiresIn))),
199 }
200
201 if err := s.db.Clauses(clause.OnConflict{
202 Columns: []clause.Column{{Name: "did"}},
203 UpdateAll: true,
204 }).Create(oauthSession).Error; err != nil {
205 return err
206 }
207
208 sess.Options = &sessions.Options{
209 Path: "/",
210 MaxAge: 86400 * 7,
211 HttpOnly: true,
212 }
213
214 // make sure the session is empty
215 sess.Values = map[any]any{}
216 sess.Values["did"] = oauthRequest.Did
217
218 if err := sess.Save(e.Request(), e.Response()); err != nil {
219 return err
220 }
221
222 slog.Default().Info("handled callback", "params", params)
223
224 return e.Redirect(302, "/")
225}
226
227func (s *TestServer) handleLogout(e echo.Context) error {
228 sess, err := session.Get("session", e)
229 if err != nil {
230 return err
231 }
232
233 sess.Options = &sessions.Options{
234 Path: "/",
235 MaxAge: -1,
236 HttpOnly: true,
237 }
238
239 if err := sess.Save(e.Request(), e.Response()); err != nil {
240 return err
241 }
242
243 return e.Redirect(302, "/")
244}