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