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