···
16
-
"github.com/bluesky-social/indigo/api/atproto"
17
-
"github.com/bluesky-social/indigo/api/bsky"
18
-
"github.com/bluesky-social/indigo/atproto/syntax"
19
-
"github.com/bluesky-social/indigo/lex/util"
20
-
"github.com/bluesky-social/indigo/xrpc"
"github.com/gorilla/sessions"
oauth "github.com/haileyok/atproto-oauth-golang"
_ "github.com/joho/godotenv/autoload"
···
"github.com/urfave/cli/v2"
31
-
"gorm.io/gorm/clause"
···
return e.JSON(200, s.jwksResponse)
217
-
func (s *TestServer) handleLoginSubmit(e echo.Context) error {
218
-
handle := e.FormValue("handle")
220
-
return e.Redirect(302, "/login?e=handle-empty")
223
-
_, herr := syntax.ParseHandle(handle)
224
-
_, derr := syntax.ParseDID(handle)
226
-
if herr != nil && derr != nil {
227
-
return e.Redirect(302, "/login?e=handle-invalid")
235
-
maybeDid, err := resolveHandle(e.Request().Context(), handle)
243
-
service, err := resolveService(ctx, did)
248
-
authserver, err := s.oauthClient.ResolvePDSAuthServer(ctx, service)
253
-
meta, err := s.oauthClient.FetchAuthServerMetadata(ctx, authserver)
258
-
dpopPrivateKey, err := oauth.GenerateKey(nil)
263
-
dpopPrivateKeyJson, err := json.Marshal(dpopPrivateKey)
268
-
parResp, err := s.oauthClient.SendParAuthRequest(
277
-
oauthRequest := &OauthRequest{
278
-
State: parResp.State,
279
-
AuthserverIss: meta.Issuer,
282
-
PkceVerifier: parResp.PkceVerifier,
283
-
DpopAuthserverNonce: parResp.DpopAuthserverNonce,
284
-
DpopPrivateJwk: string(dpopPrivateKeyJson),
287
-
if err := s.db.Create(oauthRequest).Error; err != nil {
291
-
u, _ := url.Parse(meta.AuthorizationEndpoint)
292
-
u.RawQuery = fmt.Sprintf(
293
-
"client_id=%s&request_uri=%s",
294
-
url.QueryEscape(serverMetadataUrl),
295
-
parResp.Resp["request_uri"].(string),
298
-
sess, err := session.Get("session", e)
303
-
sess.Options = &sessions.Options{
305
-
MaxAge: 300, // save for five minutes
309
-
// make sure the session is empty
310
-
sess.Values = map[interface{}]interface{}{}
311
-
sess.Values["oauth_state"] = parResp.State
312
-
sess.Values["oauth_did"] = did
314
-
if err := sess.Save(e.Request(), e.Response()); err != nil {
318
-
return e.Redirect(302, u.String())
321
-
func (s *TestServer) handleCallback(e echo.Context) error {
322
-
resState := e.QueryParam("state")
323
-
resIss := e.QueryParam("iss")
324
-
resCode := e.QueryParam("code")
326
-
sess, err := session.Get("session", e)
331
-
sessState := sess.Values["oauth_state"]
332
-
sessDid := sess.Values["oauth_did"]
334
-
if resState == "" || resIss == "" || resCode == "" || sessState == "" || sessDid == "" {
335
-
return fmt.Errorf("request missing needed parameters")
338
-
if resState != sessState {
339
-
return fmt.Errorf("session state does not match response state")
342
-
var oauthRequest OauthRequest
343
-
if err := s.db.Raw("SELECT * FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Scan(&oauthRequest).Error; err != nil {
347
-
if err := s.db.Exec("DELETE FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Error; err != nil {
351
-
if resIss != oauthRequest.AuthserverIss {
352
-
return fmt.Errorf("incoming iss did not match authserver iss")
355
-
jwk, err := oauth.ParseKeyFromBytes([]byte(oauthRequest.DpopPrivateJwk))
360
-
initialTokenResp, err := s.oauthClient.InitialTokenRequest(
361
-
e.Request().Context(),
365
-
oauthRequest.PkceVerifier,
366
-
oauthRequest.DpopAuthserverNonce,
373
-
// TODO: resolve if needed
375
-
if initialTokenResp.Resp["scope"] != scope {
376
-
return fmt.Errorf("did not receive correct scopes from token request")
379
-
oauthSession := &OauthSession{
380
-
Did: oauthRequest.Did,
381
-
PdsUrl: oauthRequest.PdsUrl,
382
-
AuthserverIss: oauthRequest.AuthserverIss,
383
-
AccessToken: initialTokenResp.Resp["access_token"].(string),
384
-
RefreshToken: initialTokenResp.Resp["refresh_token"].(string),
385
-
DpopAuthserverNonce: initialTokenResp.DpopAuthserverNonce,
386
-
DpopPrivateJwk: oauthRequest.DpopPrivateJwk,
389
-
if err := s.db.Clauses(clause.OnConflict{
390
-
Columns: []clause.Column{{Name: "did"}},
392
-
}).Create(oauthSession).Error; err != nil {
396
-
sess.Options = &sessions.Options{
402
-
// make sure the session is empty
403
-
sess.Values = map[interface{}]interface{}{}
404
-
sess.Values["did"] = oauthRequest.Did
406
-
if err := sess.Save(e.Request(), e.Response()); err != nil {
410
-
return e.Redirect(302, "/")
413
-
func (s *TestServer) handleLogout(e echo.Context) error {
414
-
sess, err := session.Get("session", e)
419
-
sess.Options = &sessions.Options{
425
-
if err := sess.Save(e.Request(), e.Response()); err != nil {
429
-
return e.Redirect(302, "/")
432
-
func (s *TestServer) handleMakePost(e echo.Context) error {
433
-
sess, err := session.Get("session", e)
438
-
did, ok := sess.Values["did"]
440
-
return e.Redirect(302, "/login")
443
-
var oauthSession OauthSession
444
-
if err := s.db.Raw("SELECT * FROM oauth_sessions WHERE did = ?", did).Scan(&oauthSession).Error; err != nil {
448
-
args, err := authedReqArgsFromSession(&oauthSession)
453
-
post := bsky.FeedPost{
454
-
Text: "hello from atproto golang oauth client",
455
-
CreatedAt: syntax.DatetimeNow().String(),
458
-
input := atproto.RepoCreateRecord_Input{
459
-
Collection: "app.bsky.feed.post",
460
-
Repo: oauthSession.Did,
461
-
Record: &util.LexiconTypeDecoder{Val: &post},
464
-
var out atproto.RepoCreateRecord_Output
465
-
if err := s.xrpcCli.Do(e.Request().Context(), args, xrpc.Procedure, "application/json", "com.atproto.repo.createRecord", nil, input, &out); err != nil {
469
-
return e.File(getFilePath("make-post.html"))
func authedReqArgsFromSession(session *OauthSession) (*oauth.XrpcAuthedRequestArgs, error) {
privateJwk, err := oauth.ParseKeyFromBytes([]byte(session.DpopPrivateJwk))
···
DpopPdsNonce: session.DpopPdsNonce,
DpopPrivateJwk: privateJwk,
488
-
func resolveHandle(ctx context.Context, handle string) (string, error) {
491
-
_, err := syntax.ParseHandle(handle)
496
-
recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
501
-
for _, rec := range recs {
502
-
if strings.HasPrefix(rec, "did=") {
503
-
did = strings.Split(rec, "did=")[1]
509
-
req, err := http.NewRequestWithContext(
512
-
fmt.Sprintf("https://%s/.well-known/atproto-did", handle),
519
-
resp, err := http.DefaultClient.Do(req)
523
-
defer resp.Body.Close()
525
-
if resp.StatusCode != http.StatusOK {
526
-
io.Copy(io.Discard, resp.Body)
527
-
return "", fmt.Errorf("unable to resolve handle")
530
-
b, err := io.ReadAll(resp.Body)
535
-
maybeDid := string(b)
537
-
if _, err := syntax.ParseDID(maybeDid); err != nil {
538
-
return "", fmt.Errorf("unable to resolve handle")
547
-
func resolveService(ctx context.Context, did string) (string, error) {
548
-
type Identity struct {
550
-
ID string `json:"id"`
551
-
Type string `json:"type"`
552
-
ServiceEndpoint string `json:"serviceEndpoint"`
557
-
if strings.HasPrefix(did, "did:plc:") {
558
-
ustr = fmt.Sprintf("https://plc.directory/%s", did)
559
-
} else if strings.HasPrefix(did, "did:web:") {
560
-
ustr = fmt.Sprintf("https://%s/.well-known/did.json", did)
562
-
return "", fmt.Errorf("did was not a supported did type")
565
-
req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
570
-
resp, err := http.DefaultClient.Do(req)
574
-
defer resp.Body.Close()
576
-
if resp.StatusCode != 200 {
577
-
io.Copy(io.Discard, resp.Body)
578
-
return "", fmt.Errorf("could not find identity in plc registry")
581
-
var identity Identity
582
-
if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
587
-
for _, svc := range identity.Service {
588
-
if svc.ID == "#atproto_pds" {
589
-
service = svc.ServiceEndpoint
594
-
return "", fmt.Errorf("could not find atproto_pds service in identity services")
597
-
return service, nil
func getFilePath(file string) string {