this repo has no description
1package main 2 3import ( 4 "encoding/json" 5 "fmt" 6 "net/url" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 "github.com/gorilla/sessions" 10 oauth "github.com/haileyok/atproto-oauth-golang" 11 "github.com/labstack/echo-contrib/session" 12 "github.com/labstack/echo/v4" 13 "gorm.io/gorm/clause" 14) 15 16func (s *TestServer) handleLoginSubmit(e echo.Context) error { 17 handle := e.FormValue("handle") 18 if handle == "" { 19 return e.Redirect(302, "/login?e=handle-empty") 20 } 21 22 _, herr := syntax.ParseHandle(handle) 23 _, derr := syntax.ParseDID(handle) 24 25 if herr != nil && derr != nil { 26 return e.Redirect(302, "/login?e=handle-invalid") 27 } 28 29 var did string 30 31 if derr == nil { 32 did = handle 33 } else { 34 maybeDid, err := resolveHandle(e.Request().Context(), handle) 35 if err != nil { 36 return err 37 } 38 39 did = maybeDid 40 } 41 42 service, err := resolveService(ctx, did) 43 if err != nil { 44 return err 45 } 46 47 authserver, err := s.oauthClient.ResolvePDSAuthServer(ctx, service) 48 if err != nil { 49 return err 50 } 51 52 meta, err := s.oauthClient.FetchAuthServerMetadata(ctx, authserver) 53 if err != nil { 54 return err 55 } 56 57 dpopPrivateKey, err := oauth.GenerateKey(nil) 58 if err != nil { 59 return err 60 } 61 62 dpopPrivateKeyJson, err := json.Marshal(dpopPrivateKey) 63 if err != nil { 64 return err 65 } 66 67 parResp, err := s.oauthClient.SendParAuthRequest( 68 ctx, 69 authserver, 70 meta, 71 "", 72 scope, 73 dpopPrivateKey, 74 ) 75 76 oauthRequest := &OauthRequest{ 77 State: parResp.State, 78 AuthserverIss: meta.Issuer, 79 Did: did, 80 PdsUrl: service, 81 PkceVerifier: parResp.PkceVerifier, 82 DpopAuthserverNonce: parResp.DpopAuthserverNonce, 83 DpopPrivateJwk: string(dpopPrivateKeyJson), 84 } 85 86 if err := s.db.Create(oauthRequest).Error; err != nil { 87 return err 88 } 89 90 u, _ := url.Parse(meta.AuthorizationEndpoint) 91 u.RawQuery = fmt.Sprintf( 92 "client_id=%s&request_uri=%s", 93 url.QueryEscape(serverMetadataUrl), 94 parResp.Resp["request_uri"].(string), 95 ) 96 97 sess, err := session.Get("session", e) 98 if err != nil { 99 return err 100 } 101 102 sess.Options = &sessions.Options{ 103 Path: "/", 104 MaxAge: 300, // save for five minutes 105 HttpOnly: true, 106 } 107 108 // make sure the session is empty 109 sess.Values = map[interface{}]interface{}{} 110 sess.Values["oauth_state"] = parResp.State 111 sess.Values["oauth_did"] = did 112 113 if err := sess.Save(e.Request(), e.Response()); err != nil { 114 return err 115 } 116 117 return e.Redirect(302, u.String()) 118} 119 120func (s *TestServer) handleCallback(e echo.Context) error { 121 resState := e.QueryParam("state") 122 resIss := e.QueryParam("iss") 123 resCode := e.QueryParam("code") 124 125 sess, err := session.Get("session", e) 126 if err != nil { 127 return err 128 } 129 130 sessState := sess.Values["oauth_state"] 131 sessDid := sess.Values["oauth_did"] 132 133 if resState == "" || resIss == "" || resCode == "" || sessState == "" || sessDid == "" { 134 return fmt.Errorf("request missing needed parameters") 135 } 136 137 if resState != sessState { 138 return fmt.Errorf("session state does not match response state") 139 } 140 141 var oauthRequest OauthRequest 142 if err := s.db.Raw("SELECT * FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Scan(&oauthRequest).Error; err != nil { 143 return err 144 } 145 146 if err := s.db.Exec("DELETE FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Error; err != nil { 147 return err 148 } 149 150 if resIss != oauthRequest.AuthserverIss { 151 return fmt.Errorf("incoming iss did not match authserver iss") 152 } 153 154 jwk, err := oauth.ParseKeyFromBytes([]byte(oauthRequest.DpopPrivateJwk)) 155 if err != nil { 156 return err 157 } 158 159 initialTokenResp, err := s.oauthClient.InitialTokenRequest( 160 e.Request().Context(), 161 resCode, 162 resIss, 163 resIss, 164 oauthRequest.PkceVerifier, 165 oauthRequest.DpopAuthserverNonce, 166 jwk, 167 ) 168 if err != nil { 169 return err 170 } 171 172 // TODO: resolve if needed 173 174 if initialTokenResp.Resp["scope"] != scope { 175 return fmt.Errorf("did not receive correct scopes from token request") 176 } 177 178 oauthSession := &OauthSession{ 179 Did: oauthRequest.Did, 180 PdsUrl: oauthRequest.PdsUrl, 181 AuthserverIss: oauthRequest.AuthserverIss, 182 AccessToken: initialTokenResp.Resp["access_token"].(string), 183 RefreshToken: initialTokenResp.Resp["refresh_token"].(string), 184 DpopAuthserverNonce: initialTokenResp.DpopAuthserverNonce, 185 DpopPrivateJwk: oauthRequest.DpopPrivateJwk, 186 } 187 188 if err := s.db.Clauses(clause.OnConflict{ 189 Columns: []clause.Column{{Name: "did"}}, 190 UpdateAll: true, 191 }).Create(oauthSession).Error; err != nil { 192 return err 193 } 194 195 sess.Options = &sessions.Options{ 196 Path: "/", 197 MaxAge: 86400 * 7, 198 HttpOnly: true, 199 } 200 201 // make sure the session is empty 202 sess.Values = map[interface{}]interface{}{} 203 sess.Values["did"] = oauthRequest.Did 204 205 if err := sess.Save(e.Request(), e.Response()); err != nil { 206 return err 207 } 208 209 return e.Redirect(302, "/") 210} 211 212func (s *TestServer) handleLogout(e echo.Context) error { 213 sess, err := session.Get("session", e) 214 if err != nil { 215 return err 216 } 217 218 sess.Options = &sessions.Options{ 219 Path: "/", 220 MaxAge: -1, 221 HttpOnly: true, 222 } 223 224 if err := sess.Save(e.Request(), e.Response()); err != nil { 225 return err 226 } 227 228 return e.Redirect(302, "/") 229}