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}