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