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