this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'main' into DavidBuchanan314/oauth-revocation

authored by

David Buchanan and committed by
GitHub
d71773b1 00501ca4

+261 -30
+7 -1
atproto/auth/oauth/cmd/oauth-web-demo/main.go
··· 8 8 "log/slog" 9 9 "net/http" 10 10 "os" 11 + "slices" 11 12 12 13 _ "github.com/joho/godotenv/autoload" 13 14 ··· 78 79 79 80 func runServer(cctx *cli.Context) error { 80 81 81 - scopes := []string{"atproto", "transition:generic"} 82 + // the 'account:email' scope is requested only as a demo of users not granting a permission during auth flow 83 + scopes := []string{"atproto", "repo:app.bsky.feed.post?action=create", "account:email"} 82 84 bind := ":8080" 83 85 84 86 var config oauth.ClientConfig ··· 245 247 if err != nil { 246 248 http.Error(w, fmt.Errorf("processing OAuth callback: %w", err).Error(), http.StatusBadRequest) 247 249 return 250 + } 251 + 252 + if !slices.Equal(sessData.Scopes, s.OAuth.Config.Scopes) { 253 + slog.Warn("session auth scopes did not match those requested", "requested", s.OAuth.Config.Scopes, "granted", sessData.Scopes) 248 254 } 249 255 250 256 // create signed cookie session, indicating account DID
+42 -11
atproto/auth/oauth/oauth.go
··· 242 242 } 243 243 claims := clientAssertionClaims{ 244 244 RegisteredClaims: jwt.RegisteredClaims{ 245 - Issuer: cfg.ClientID, 246 - Subject: cfg.ClientID, 247 - Audience: []string{authURL}, 248 - ID: secureRandomBase64(16), 249 - IssuedAt: jwt.NewNumericDate(time.Now()), 245 + Issuer: cfg.ClientID, 246 + Subject: cfg.ClientID, 247 + Audience: []string{authURL}, 248 + ID: secureRandomBase64(16), 249 + IssuedAt: jwt.NewNumericDate(time.Now()), 250 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtExpirationDuration)), 250 251 }, 251 252 } 252 253 ··· 580 581 581 582 // High-level helper for completing auth flow: verifies callback query parameters against persisted auth request info, makes initial token request to the auth server, validates account identifier, and persists session data. 582 583 func (app *ClientApp) ProcessCallback(ctx context.Context, params url.Values) (*ClientSessionData, error) { 584 + // There are two callback response formats, for error and non-error conditions, each expecting different 585 + // parameters. 586 + // 587 + // Error responses expect: state, error (and optionally: error_description, error_uri) 588 + // Non-error responses expect: state, iss, code 583 589 584 590 state := params.Get("state") 585 - authserverURL := params.Get("iss") 586 - authCode := params.Get("code") 587 - if state == "" || authserverURL == "" || authCode == "" { 588 - return nil, fmt.Errorf("missing required query param") 591 + if state == "" { 592 + return nil, fmt.Errorf("missing state query param") 589 593 } 590 594 591 595 info, err := app.Store.GetAuthRequestInfo(ctx, state) 592 596 if err != nil { 593 597 return nil, fmt.Errorf("loading auth request info: %w", err) 594 598 } 599 + // This check should never fail, but it guards against a faulty ClientAuthStore implementation 600 + if info.State != state { 601 + return nil, fmt.Errorf("callback state doesn't match request info") 602 + } 595 603 596 - if info.State != state || info.AuthServerURL != authserverURL { 597 - return nil, fmt.Errorf("callback params don't match request info") 604 + // NOTE: A corresponding `state` is expected even under error conditions, 605 + // hence we check error *after* checking state. 606 + errorCode := params.Get("error") 607 + if errorCode != "" { 608 + var errorUri *syntax.URI 609 + parsedUri, err := syntax.ParseURI(params.Get("error_uri")) 610 + if err == nil { 611 + errorUri = &parsedUri 612 + } 613 + return nil, &AuthRequestCallbackError{ 614 + ErrorCode: errorCode, 615 + ErrorDescription: params.Get("error_description"), 616 + ErrorURI: errorUri, 617 + } 618 + } 619 + 620 + // If we reached here, there was no `error` and we can process the rest of the parameters 621 + authserverURL := params.Get("iss") 622 + authCode := params.Get("code") 623 + if authserverURL == "" || authCode == "" { 624 + return nil, fmt.Errorf("missing required query param") 625 + } 626 + 627 + if info.AuthServerURL != authserverURL { 628 + return nil, fmt.Errorf("callback iss doesn't match request info") 598 629 } 599 630 600 631 tokenResp, err := app.SendInitialTokenRequest(ctx, authCode, *info)
+20
atproto/auth/oauth/types.go
··· 433 433 // For confidential clients, the signed client assertion JWT 434 434 ClientAssertion *string `url:"client_assertion"` 435 435 } 436 + 437 + // Returned by [ClientApp.ProcessCallback] if the AS signals an error in the redirect URL parameters, per rfc6749 section 4.1.2.1 438 + // 439 + // NOTE: This is untrusted data and should not be e.g. rendered to HTML without appropriate escaping 440 + type AuthRequestCallbackError struct { 441 + ErrorCode string 442 + ErrorDescription string 443 + ErrorURI *syntax.URI 444 + } 445 + 446 + func (e *AuthRequestCallbackError) Error() string { 447 + res := "OAuth request callback error: " + e.ErrorCode 448 + if e.ErrorDescription != "" { 449 + res += ": " + e.ErrorDescription 450 + } 451 + if e.ErrorURI != nil { 452 + res += " (" + e.ErrorURI.String() + ")" 453 + } 454 + return res 455 + }
+1 -1
atproto/client/apirequest.go
··· 34 34 // Optional query parameters (field may be nil). These will be encoded as provided. 35 35 QueryParams url.Values 36 36 37 - // Optional HTTP headers (field bay be nil). Only the first value will be included for each header key ("Set" behavior). 37 + // Optional HTTP headers (field may be nil). Only the first value will be included for each header key ("Set" behavior). 38 38 Headers http.Header 39 39 } 40 40
+37 -16
atproto/lexicon/language.go
··· 83 83 } 84 84 s.Inner = v 85 85 case SchemaQuery: 86 - for i, val := range v.Parameters.Properties { 87 - val.SetBase(base) 88 - v.Parameters.Properties[i] = val 86 + if v.Parameters != nil { 87 + for i, val := range v.Parameters.Properties { 88 + val.SetBase(base) 89 + v.Parameters.Properties[i] = val 90 + } 89 91 } 90 92 if v.Output != nil && v.Output.Schema != nil { 91 93 v.Output.Schema.SetBase(base) 92 94 } 93 95 s.Inner = v 94 96 case SchemaProcedure: 95 - for i, val := range v.Parameters.Properties { 96 - val.SetBase(base) 97 - v.Parameters.Properties[i] = val 97 + if v.Parameters != nil { 98 + for i, val := range v.Parameters.Properties { 99 + val.SetBase(base) 100 + v.Parameters.Properties[i] = val 101 + } 98 102 } 99 103 if v.Input != nil && v.Input.Schema != nil { 100 104 v.Input.Schema.SetBase(base) ··· 104 108 } 105 109 s.Inner = v 106 110 case SchemaSubscription: 107 - for i, val := range v.Parameters.Properties { 108 - val.SetBase(base) 109 - v.Parameters.Properties[i] = val 111 + if v.Parameters != nil { 112 + for i, val := range v.Parameters.Properties { 113 + val.SetBase(base) 114 + v.Parameters.Properties[i] = val 115 + } 110 116 } 111 117 if v.Message != nil { 112 118 v.Message.Schema.SetBase(base) ··· 326 332 type SchemaQuery struct { 327 333 Type string `json:"type"` // "query" 328 334 Description *string `json:"description,omitempty"` 329 - Parameters SchemaParams `json:"parameters"` 330 - Output *SchemaBody `json:"output"` 335 + Parameters *SchemaParams `json:"parameters"` // optional 336 + Output *SchemaBody `json:"output"` // optional 331 337 Errors []SchemaError `json:"errors,omitempty"` // optional 332 338 } 333 339 ··· 337 343 return err 338 344 } 339 345 } 346 + if s.Parameters != nil { 347 + if err := s.Parameters.CheckSchema(); err != nil { 348 + return err 349 + } 350 + } 340 351 for _, e := range s.Errors { 341 352 if err := e.CheckSchema(); err != nil { 342 353 return err 343 354 } 344 355 } 345 - return s.Parameters.CheckSchema() 356 + return nil 346 357 } 347 358 348 359 type SchemaProcedure struct { 349 360 Type string `json:"type"` // "procedure" 350 361 Description *string `json:"description,omitempty"` 351 - Parameters SchemaParams `json:"parameters"` 362 + Parameters *SchemaParams `json:"parameters"` // optional 352 363 Output *SchemaBody `json:"output"` // optional 353 364 Errors []SchemaError `json:"errors,omitempty"` // optional 354 365 Input *SchemaBody `json:"input"` // optional ··· 365 376 return err 366 377 } 367 378 } 379 + if s.Parameters != nil { 380 + if err := s.Parameters.CheckSchema(); err != nil { 381 + return err 382 + } 383 + } 368 384 for _, e := range s.Errors { 369 385 if err := e.CheckSchema(); err != nil { 370 386 return err 371 387 } 372 388 } 373 - return s.Parameters.CheckSchema() 389 + return nil 374 390 } 375 391 376 392 type SchemaSubscription struct { 377 393 Type string `json:"type"` // "subscription" 378 394 Description *string `json:"description,omitempty"` 379 - Parameters SchemaParams `json:"parameters"` 395 + Parameters *SchemaParams `json:"parameters"` // optional 380 396 Message *SchemaMessage `json:"message,omitempty"` // TODO(specs): is this really optional? 381 397 } 382 398 ··· 386 402 return err 387 403 } 388 404 } 389 - return s.Parameters.CheckSchema() 405 + if s.Parameters != nil { 406 + if err := s.Parameters.CheckSchema(); err != nil { 407 + return err 408 + } 409 + } 410 + return nil 390 411 } 391 412 392 413 type SchemaPermissionSet struct {
+16
atproto/lexicon/testdata/catalog/minimal-procedure.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "example.lexicon.minimal.procedure", 4 + "description": "demonstrates lexicon features for the procedure type", 5 + "defs": { 6 + "main": { 7 + "type": "procedure", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object" 12 + } 13 + } 14 + } 15 + } 16 + }
+12
atproto/lexicon/testdata/catalog/minimal-query.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "example.lexicon.minimal.query", 4 + "revision": 1, 5 + "description": "exercizes many lexicon features for the query type", 6 + "defs": { 7 + "main": { 8 + "type": "query", 9 + "description": "a query type" 10 + } 11 + } 12 + }
+78
atproto/lexicon/testdata/catalog/procedure.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "example.lexicon.procedure", 4 + "description": "demonstrates lexicon features for the procedure type", 5 + "defs": { 6 + "main": { 7 + "type": "procedure", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "boolean": { 12 + "type": "boolean", 13 + "description": "field of type boolean" 14 + }, 15 + "integer": { 16 + "type": "integer", 17 + "description": "field of type integer" 18 + }, 19 + "string": { 20 + "type": "string", 21 + "description": "field of type string" 22 + } 23 + } 24 + }, 25 + "input": { 26 + "encoding": "application/json", 27 + "schema": { 28 + "type": "object", 29 + "required": [ 30 + "preferences" 31 + ], 32 + "properties": { 33 + "preferences": { 34 + "type": "ref", 35 + "ref": "app.bsky.actor.defs#preferences" 36 + } 37 + } 38 + } 39 + }, 40 + "output": { 41 + "encoding": "application/json", 42 + "schema": { 43 + "type": "object", 44 + "required": [], 45 + "properties": { 46 + "blob": { 47 + "type": "blob", 48 + "description": "field of type blob" 49 + }, 50 + "unknown": { 51 + "type": "unknown", 52 + "description": "field of type unknown" 53 + }, 54 + "array": { 55 + "type": "array", 56 + "description": "field of type array", 57 + "items": { 58 + "type": "integer" 59 + } 60 + }, 61 + "object": { 62 + "type": "object", 63 + "description": "field of type null", 64 + "properties": { 65 + "a": { 66 + "type": "integer" 67 + }, 68 + "b": { 69 + "type": "integer" 70 + } 71 + } 72 + } 73 + } 74 + } 75 + } 76 + } 77 + } 78 + }
+1 -1
atproto/lexicon/testdata/catalog/record.json
··· 2 2 "lexicon": 1, 3 3 "id": "example.lexicon.record", 4 4 "revision": 1, 5 - "description": "exercizes many lexicon features for the record type", 5 + "description": "demonstrates lexicon features for the record type", 6 6 "defs": { 7 7 "main": { 8 8 "type": "record",
+47
atproto/lexicon/testdata/catalog/subscription.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "example.lexicon.subscription", 4 + "description": "demonstrates lexicon features for the subscription type", 5 + "defs": { 6 + "main": { 7 + "type": "subscription", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "cursor": { 12 + "type": "integer", 13 + "description": "start at the given sequence number" 14 + } 15 + } 16 + }, 17 + "message": { 18 + "schema": { 19 + "type": "union", 20 + "refs": ["#yo", "#info"] 21 + } 22 + }, 23 + "errors": [{ "name": "FutureCursor" }] 24 + }, 25 + "yo": { 26 + "type": "object", 27 + "required": ["seq", "yo"], 28 + "properties": { 29 + "seq": { "type": "integer" }, 30 + "yo": { "type": "boolean" } 31 + } 32 + }, 33 + "info": { 34 + "type": "object", 35 + "required": ["name"], 36 + "properties": { 37 + "name": { 38 + "type": "string", 39 + "knownValues": ["OutdatedCursor"] 40 + }, 41 + "message": { 42 + "type": "string" 43 + } 44 + } 45 + } 46 + } 47 + }