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}