···88 "log/slog"
99 "net/http"
1010 "os"
1111+ "slices"
11121213 _ "github.com/joho/godotenv/autoload"
1314···78797980func runServer(cctx *cli.Context) error {
80818181- scopes := []string{"atproto", "transition:generic"}
8282+ // the 'account:email' scope is requested only as a demo of users not granting a permission during auth flow
8383+ scopes := []string{"atproto", "repo:app.bsky.feed.post?action=create", "account:email"}
8284 bind := ":8080"
83858486 var config oauth.ClientConfig
···245247 if err != nil {
246248 http.Error(w, fmt.Errorf("processing OAuth callback: %w", err).Error(), http.StatusBadRequest)
247249 return
250250+ }
251251+252252+ if !slices.Equal(sessData.Scopes, s.OAuth.Config.Scopes) {
253253+ slog.Warn("session auth scopes did not match those requested", "requested", s.OAuth.Config.Scopes, "granted", sessData.Scopes)
248254 }
249255250256 // create signed cookie session, indicating account DID
+42-11
atproto/auth/oauth/oauth.go
···242242 }
243243 claims := clientAssertionClaims{
244244 RegisteredClaims: jwt.RegisteredClaims{
245245- Issuer: cfg.ClientID,
246246- Subject: cfg.ClientID,
247247- Audience: []string{authURL},
248248- ID: secureRandomBase64(16),
249249- IssuedAt: jwt.NewNumericDate(time.Now()),
245245+ Issuer: cfg.ClientID,
246246+ Subject: cfg.ClientID,
247247+ Audience: []string{authURL},
248248+ ID: secureRandomBase64(16),
249249+ IssuedAt: jwt.NewNumericDate(time.Now()),
250250+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtExpirationDuration)),
250251 },
251252 }
252253···580581581582// 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.
582583func (app *ClientApp) ProcessCallback(ctx context.Context, params url.Values) (*ClientSessionData, error) {
584584+ // There are two callback response formats, for error and non-error conditions, each expecting different
585585+ // parameters.
586586+ //
587587+ // Error responses expect: state, error (and optionally: error_description, error_uri)
588588+ // Non-error responses expect: state, iss, code
583589584590 state := params.Get("state")
585585- authserverURL := params.Get("iss")
586586- authCode := params.Get("code")
587587- if state == "" || authserverURL == "" || authCode == "" {
588588- return nil, fmt.Errorf("missing required query param")
591591+ if state == "" {
592592+ return nil, fmt.Errorf("missing state query param")
589593 }
590594591595 info, err := app.Store.GetAuthRequestInfo(ctx, state)
592596 if err != nil {
593597 return nil, fmt.Errorf("loading auth request info: %w", err)
594598 }
599599+ // This check should never fail, but it guards against a faulty ClientAuthStore implementation
600600+ if info.State != state {
601601+ return nil, fmt.Errorf("callback state doesn't match request info")
602602+ }
595603596596- if info.State != state || info.AuthServerURL != authserverURL {
597597- return nil, fmt.Errorf("callback params don't match request info")
604604+ // NOTE: A corresponding `state` is expected even under error conditions,
605605+ // hence we check error *after* checking state.
606606+ errorCode := params.Get("error")
607607+ if errorCode != "" {
608608+ var errorUri *syntax.URI
609609+ parsedUri, err := syntax.ParseURI(params.Get("error_uri"))
610610+ if err == nil {
611611+ errorUri = &parsedUri
612612+ }
613613+ return nil, &AuthRequestCallbackError{
614614+ ErrorCode: errorCode,
615615+ ErrorDescription: params.Get("error_description"),
616616+ ErrorURI: errorUri,
617617+ }
618618+ }
619619+620620+ // If we reached here, there was no `error` and we can process the rest of the parameters
621621+ authserverURL := params.Get("iss")
622622+ authCode := params.Get("code")
623623+ if authserverURL == "" || authCode == "" {
624624+ return nil, fmt.Errorf("missing required query param")
625625+ }
626626+627627+ if info.AuthServerURL != authserverURL {
628628+ return nil, fmt.Errorf("callback iss doesn't match request info")
598629 }
599630600631 tokenResp, err := app.SendInitialTokenRequest(ctx, authCode, *info)
+20
atproto/auth/oauth/types.go
···433433 // For confidential clients, the signed client assertion JWT
434434 ClientAssertion *string `url:"client_assertion"`
435435}
436436+437437+// Returned by [ClientApp.ProcessCallback] if the AS signals an error in the redirect URL parameters, per rfc6749 section 4.1.2.1
438438+//
439439+// NOTE: This is untrusted data and should not be e.g. rendered to HTML without appropriate escaping
440440+type AuthRequestCallbackError struct {
441441+ ErrorCode string
442442+ ErrorDescription string
443443+ ErrorURI *syntax.URI
444444+}
445445+446446+func (e *AuthRequestCallbackError) Error() string {
447447+ res := "OAuth request callback error: " + e.ErrorCode
448448+ if e.ErrorDescription != "" {
449449+ res += ": " + e.ErrorDescription
450450+ }
451451+ if e.ErrorURI != nil {
452452+ res += " (" + e.ErrorURI.String() + ")"
453453+ }
454454+ return res
455455+}
+1-1
atproto/client/apirequest.go
···3434 // Optional query parameters (field may be nil). These will be encoded as provided.
3535 QueryParams url.Values
36363737- // Optional HTTP headers (field bay be nil). Only the first value will be included for each header key ("Set" behavior).
3737+ // Optional HTTP headers (field may be nil). Only the first value will be included for each header key ("Set" behavior).
3838 Headers http.Header
3939}
4040
+37-16
atproto/lexicon/language.go
···8383 }
8484 s.Inner = v
8585 case SchemaQuery:
8686- for i, val := range v.Parameters.Properties {
8787- val.SetBase(base)
8888- v.Parameters.Properties[i] = val
8686+ if v.Parameters != nil {
8787+ for i, val := range v.Parameters.Properties {
8888+ val.SetBase(base)
8989+ v.Parameters.Properties[i] = val
9090+ }
8991 }
9092 if v.Output != nil && v.Output.Schema != nil {
9193 v.Output.Schema.SetBase(base)
9294 }
9395 s.Inner = v
9496 case SchemaProcedure:
9595- for i, val := range v.Parameters.Properties {
9696- val.SetBase(base)
9797- v.Parameters.Properties[i] = val
9797+ if v.Parameters != nil {
9898+ for i, val := range v.Parameters.Properties {
9999+ val.SetBase(base)
100100+ v.Parameters.Properties[i] = val
101101+ }
98102 }
99103 if v.Input != nil && v.Input.Schema != nil {
100104 v.Input.Schema.SetBase(base)
···104108 }
105109 s.Inner = v
106110 case SchemaSubscription:
107107- for i, val := range v.Parameters.Properties {
108108- val.SetBase(base)
109109- v.Parameters.Properties[i] = val
111111+ if v.Parameters != nil {
112112+ for i, val := range v.Parameters.Properties {
113113+ val.SetBase(base)
114114+ v.Parameters.Properties[i] = val
115115+ }
110116 }
111117 if v.Message != nil {
112118 v.Message.Schema.SetBase(base)
···326332type SchemaQuery struct {
327333 Type string `json:"type"` // "query"
328334 Description *string `json:"description,omitempty"`
329329- Parameters SchemaParams `json:"parameters"`
330330- Output *SchemaBody `json:"output"`
335335+ Parameters *SchemaParams `json:"parameters"` // optional
336336+ Output *SchemaBody `json:"output"` // optional
331337 Errors []SchemaError `json:"errors,omitempty"` // optional
332338}
333339···337343 return err
338344 }
339345 }
346346+ if s.Parameters != nil {
347347+ if err := s.Parameters.CheckSchema(); err != nil {
348348+ return err
349349+ }
350350+ }
340351 for _, e := range s.Errors {
341352 if err := e.CheckSchema(); err != nil {
342353 return err
343354 }
344355 }
345345- return s.Parameters.CheckSchema()
356356+ return nil
346357}
347358348359type SchemaProcedure struct {
349360 Type string `json:"type"` // "procedure"
350361 Description *string `json:"description,omitempty"`
351351- Parameters SchemaParams `json:"parameters"`
362362+ Parameters *SchemaParams `json:"parameters"` // optional
352363 Output *SchemaBody `json:"output"` // optional
353364 Errors []SchemaError `json:"errors,omitempty"` // optional
354365 Input *SchemaBody `json:"input"` // optional
···365376 return err
366377 }
367378 }
379379+ if s.Parameters != nil {
380380+ if err := s.Parameters.CheckSchema(); err != nil {
381381+ return err
382382+ }
383383+ }
368384 for _, e := range s.Errors {
369385 if err := e.CheckSchema(); err != nil {
370386 return err
371387 }
372388 }
373373- return s.Parameters.CheckSchema()
389389+ return nil
374390}
375391376392type SchemaSubscription struct {
377393 Type string `json:"type"` // "subscription"
378394 Description *string `json:"description,omitempty"`
379379- Parameters SchemaParams `json:"parameters"`
395395+ Parameters *SchemaParams `json:"parameters"` // optional
380396 Message *SchemaMessage `json:"message,omitempty"` // TODO(specs): is this really optional?
381397}
382398···386402 return err
387403 }
388404 }
389389- return s.Parameters.CheckSchema()
405405+ if s.Parameters != nil {
406406+ if err := s.Parameters.CheckSchema(); err != nil {
407407+ return err
408408+ }
409409+ }
410410+ return nil
390411}
391412392413type SchemaPermissionSet struct {
···22 "lexicon": 1,
33 "id": "example.lexicon.record",
44 "revision": 1,
55- "description": "exercizes many lexicon features for the record type",
55+ "description": "demonstrates lexicon features for the record type",
66 "defs": {
77 "main": {
88 "type": "record",