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