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