···1919 "github.com/google/go-querystring/query"
2020)
21212222-var JWT_EXPIRATION_DURATION = 30 * time.Second
2222+var jwtExpirationDuration = 30 * time.Second
23232424// Service-level client. Used to establish and refrsh OAuth sessions, but is not itself account or session specific, and can not be used directly to make API calls on behalf of a user.
2525type ClientApp struct {
···3030 Store ClientAuthStore
3131}
32323333+// App-level client configuration data.
3434+//
3535+// Not to be confused with the [ClientMetadata] struct type, which represents a full client metadata JSON document.
3336type ClientConfig struct {
3434- ClientID string
3737+ // Full client identifier, which should be an HTTP URL
3838+ ClientID string
3939+ // Fully qualified callback URL
3540 CallbackURL string
3636- // set of scope strings; must include "atproto"
3737- Scopes []string
3838-4141+ // Set of OAuth scope strings, which will be both declared in client metadata document and requested for every session. Must include "atproto".
4242+ Scopes []string
3943 UserAgent string
40444145 // For confidential clients, the private client assertion key. Note that while an interface is used here, only P-256 is allowed by the current specification.
···4549 KeyID *string
4650}
47515252+// Constructs a [ClientApp] based on configuration.
4853func NewClientApp(config *ClientConfig, store ClientAuthStore) *ClientApp {
4954 app := &ClientApp{
5055 Client: http.DefaultClient,
···102107 return c
103108}
104109110110+// Whether this is a "confidential" OAuth client (with configured client attestation key), versus "public" client.
105111func (config *ClientConfig) IsConfidential() bool {
106112 return config.PrivateKey != nil && config.KeyID != nil
107113}
···151157 return strings.Join(scopes, " ")
152158}
153159154154-// Returns a ClientMetadata struct with the required fields populated based on this client configuration. Clients may want to populate additional metadata fields on top of this response.
160160+// Returns a [ClientMetadata] struct with the required fields populated based on this client configuration. Clients may want to populate additional metadata fields on top of this response.
155161//
156162// NOTE: confidential clients currently must provide JWKSUri after the fact
157163func (config *ClientConfig) ClientMetadata() ClientMetadata {
···177183 return m
178184}
179185186186+// High-level helper for fetching session data from store, based on account DID and session identifier.
180187func (app *ClientApp) ResumeSession(ctx context.Context, did syntax.DID, sessionID string) (*ClientSession, error) {
181188182189 sd, err := app.Store.GetSession(ctx, did, sessionID)
···228235 Nonce *string `json:"nonce,omitempty"`
229236}
230237238238+// Low-level helper to generate and sign an OAuth confidential client assertion token (JWT).
231239func (cfg *ClientConfig) NewClientAssertion(authURL string) (string, error) {
232240 if !cfg.IsConfidential() {
233241 return "", fmt.Errorf("non-confidential client")
···263271 RegisteredClaims: jwt.RegisteredClaims{
264272 ID: secureRandomBase64(16),
265273 IssuedAt: jwt.NewNumericDate(time.Now()),
266266- ExpiresAt: jwt.NewNumericDate(time.Now().Add(JWT_EXPIRATION_DURATION)),
274274+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtExpirationDuration)),
267275 },
268276 }
269277 if dpopNonce != "" {
···303311 return fmt.Sprintf("%s", errResp["error"])
304312}
305313306306-// Sends PAR request to auth server
314314+// Low-level helper to send PAR request to auth server, which involves starting PKCE and DPoP.
307315func (app *ClientApp) SendAuthRequest(ctx context.Context, authMeta *AuthServerMetadata, scope, loginHint string) (*AuthRequestData, error) {
308316309317 parURL := authMeta.PushedAuthorizationRequestEndpoint
···330338 if err != nil {
331339 return nil, err
332340 }
333333- body.ClientAssertionType = CLIENT_ASSERTION_JWT_BEARER
341341+ body.ClientAssertionType = ClientAssertionJWTBearer
334342 body.ClientAssertion = assertionJWT
335343 }
336344···416424 return &parInfo, nil
417425}
418426427427+// Lower-level helper. This is usually invoked as part of [ProcessCallback].
419428func (app *ClientApp) SendInitialTokenRequest(ctx context.Context, authCode string, info AuthRequestData) (*TokenResponse, error) {
420429421430 // TODO: don't re-fetch? caching?
···437446 if err != nil {
438447 return nil, err
439448 }
440440- body.ClientAssertionType = &CLIENT_ASSERTION_JWT_BEARER
449449+ body.ClientAssertionType = &ClientAssertionJWTBearer
441450 body.ClientAssertion = &clientAssertion
442451 }
443452···509518 return &tokenResp, nil
510519}
511520521521+// High-level helper for starting a new session. Resolves identifier to resource server and auth server metadata, sends PAR request, persists request info to store, and returns a redirect URL.
522522+//
523523+// The `identifier` argument can be an atproto account identifier (handle or DID), or can be a URL to the account's auth server.
524524+//
525525+// The returned sting will be a web URL that the user should be redirected to (in browser) to approve the auth flow.
512526func (app *ClientApp) StartAuthFlow(ctx context.Context, identifier string) (string, error) {
513527514528 var authserverURL string
···568582 return redirectURL, nil
569583}
570584585585+// 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.
571586func (app *ClientApp) ProcessCallback(ctx context.Context, params url.Values) (*ClientSessionData, error) {
572587573588 state := params.Get("state")
+3-4
atproto/auth/oauth/resolver.go
···13131414// Helper for resolving OAuth documents from the public web: client metadata, auth server metadata, etc.
1515//
1616-// NOTE: will probably want to add flexible caching to this interface, and that may mean turning it in to an interface.
1616+// NOTE: configurable caching will likely be added in the future, but is not implemented yet. This struct may become an interface to support more flexible caching and resolution policies.
1717type Resolver struct {
1818 Client *http.Client
1919 UserAgent string
···8383 return authURL, nil
8484}
85858686-// Resolves an Auth Server URL to server metadata.
8787-//
8888-// Validates the auth server metadata before returning.
8686+// Resolves an Auth Server URL to server metadata. Validates metadata before returning.
8987func (r *Resolver) ResolveAuthServerMetadata(ctx context.Context, serverURL string) (*AuthServerMetadata, error) {
9088 u, err := url.Parse(serverURL)
9189 if err != nil {
···129127 return &body, nil
130128}
131129130130+// Fetches and validates OAuth client metadata document based on identifier in URL format.
132131func (r *Resolver) ResolveClientMetadata(ctx context.Context, clientID string) (*ClientMetadata, error) {
133132 u, err := url.Parse(clientID)
134133 if err != nil {
+6-2
atproto/auth/oauth/session.go
···5757 // TODO: also persist access token creation time / expiration time? In context that token might not be an easily parsed JWT
5858}
59596060+// Implementation of [client.AuthMethod] for an OAuth session. Handles DPoP request token signing and nonce rotation, and token refresh requests. Optionally uses a callback to persist updated session data.
6161+//
6262+// A single ClientSession instance can be called concurrently: updates to session data (the 'Data' field) are protected with a RW mutex lock. Note that concurrent calls to distinct ClientSession instances for the same session could result in clobbered session data.
6063type ClientSession struct {
6164 // HTTP client used for token refresh requests
6265 Client *http.Client
···9093 if err != nil {
9194 return "", err
9295 }
9393- body.ClientAssertionType = &CLIENT_ASSERTION_JWT_BEARER
9696+ body.ClientAssertionType = &ClientAssertionJWTBearer
9497 body.ClientAssertion = &clientAssertion
9598 }
9699···180183 Issuer: sess.Data.AuthServerURL,
181184 ID: secureRandomBase64(16),
182185 IssuedAt: jwt.NewNumericDate(time.Now()),
183183- ExpiresAt: jwt.NewNumericDate(time.Now().Add(JWT_EXPIRATION_DURATION)),
186186+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtExpirationDuration)),
184187 },
185188 }
186189 if sess.Data.DPoPHostNonce != "" {
···335338 return nil, fmt.Errorf("OAuth client ran out of request retries")
336339}
337340341341+// Creates a new [client.APIClient] which wraps this session for auth.
338342func (sess *ClientSession) APIClient() *client.APIClient {
339343 c := client.APIClient{
340344 Client: sess.Client,
+4-4
atproto/auth/oauth/types.go
···1111 "github.com/bluesky-social/indigo/atproto/syntax"
1212)
13131414-var CLIENT_ASSERTION_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
1414+var ClientAssertionJWTBearer string = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
15151616var (
1717 ErrInvalidAuthServerMetadata = errors.New("invalid auth server metadata")
···6262 JWKS *JWKS `json:"jwks,omitempty"`
63636464 // URL pointing to a JWKS JSON object. See `jwks` above for details.
6565- JWKSUri *string `json:"jwks_uri,omitempty"`
6565+ JWKSURI *string `json:"jwks_uri,omitempty"`
66666767 // human-readable name of the client
6868 ClientName *string `json:"client_name,omitempty"`
···82828383// returns 'true' if client metadata indicates that this is a confidential client
8484func (m *ClientMetadata) IsConfidential() bool {
8585- if (m.JWKSUri != nil || (m.JWKS != nil && len(m.JWKS.Keys) > 0)) && m.TokenEndpointAuthMethod == "private_key_jwt" {
8585+ if (m.JWKSURI != nil || (m.JWKS != nil && len(m.JWKS.Keys) > 0)) && m.TokenEndpointAuthMethod == "private_key_jwt" {
8686 return true
8787 }
8888···142142 return fmt.Errorf("%w: dpop_bound_access_tokens must be true (DPoP is required)", ErrInvalidClientMetadata)
143143 }
144144145145- if m.JWKSUri != nil && *m.JWKSUri == "" {
145145+ if m.JWKSURI != nil && *m.JWKSURI == "" {
146146 return fmt.Errorf("%w: jwks_uri must be valid URL (when provided)", ErrInvalidClientMetadata)
147147 }
148148