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