Skip to content

Proposal: Client API support for saving and restoring OAuth2 sessions #901

@smlx

Description

@smlx

Is your feature request related to a problem? Please describe.

I am adding MCP OAuth2 support to a client and don't want my users to be re-prompted for authorization if there is still a valid refresh token from their last session.

Currently, clients using AuthorizationCodeHandler and StreamableClientTransport do not have a built-in mechanism to persist and restore OAuth2 sessions across application restarts.

Describe the solution you'd like

I propose adding an API to support saving and restoring OAuth2 sessions.

The specific additions to the auth package API are:

  1. Add a SessionSaver callback function to AuthorizationCodeHandlerConfig:
	// SessionSaver is an optional function that can be set to save oauth2
	// sessions to persistent storage. If it is set:
	//  - It is called immediately after the authorization code is successfully
	//    exchanged for a token in [AuthorizationCodeHandler.Authorize].
	//  - After a successful call to [AuthorizationCodeHandler.Authorize], the
	//    token source returned by [AuthorizationCodeHandler.TokenSource] will be
	//    a [NewSavingTokenSource].
	// To ensure SessionSaver is called on subsequent token refreshes, clients
	// must use the token source returned by [AuthorizationCodeHandler.TokenSource]
	// (for example, by returning it from your own [OAuthHandler.TokenSource]
	// implementation) after [AuthorizationCodeHandler.Authorize] returns
	// successfully.
	SessionSaver func(*oauth2.Config, *oauth2.Token)
  1. Add a NewSavingTokenSource function:
// NewSavingTokenSource persists OAuth 2.0 sessions by intercepting token
// refreshes from the wrapped [oauth2.TokenSource]. When this wrapper detects
// an access token refresh, it calls the provided session saver with the
// [oauth2.Config] and the new [oauth2.Token].
//
// The initial token argument prevents saver from being called unnecessarily if
// a valid token already exists. If initial is invalid or nil, saver may be
// called once before the wrapped token is refreshed.
//
// Clients implementing [OAuthHandler.TokenSource] can use this constructor to
// wrap token sources loaded from storage to ensure that subsequent refreshes
// during the application's lifetime are intercepted.
func NewSavingTokenSource(wrapped oauth2.TokenSource, config *oauth2.Config, initialToken *oauth2.Token, saver func(*oauth2.Config, *oauth2.Token)) oauth2.TokenSource

This design is heavily inspired by the initial design in #785. The config argument was added so that the entire oauth2 session can be persisted. The context was removed as I didn't understand the need for it. I haven't found it necessary but am open to re-adding it if there is a good reason.

The two individual API additions handle saving the oauth2 session during its two phases:

  1. After the authorization code exchange in Authorize().
  2. Later when the access token expires and is automatically refreshed.

This design is more complex than I would like because it requires passing a session saver function in two places to manage these two phases of the token lifecycle. However I wasn't able to come up with a simpler abstraction within the constraints of the existing API.

Describe alternatives you've considered

Individual SaveConfig / SaveToken / RestoreConfig / RestoreToken methods/fields.

The lifecycle of Token and Config does not align. oauth2.Token will change on every refresh. oauth2.Config will change only during Authorize(). So the minimal work required during a token refresh would be to call SaveToken. However this design has several problems:

  • What the client actually cares about is the "session". The Token is useless without the Config. So it is likely that clients will save Token and Config together as "session state".
  • It is a larger API surface and relies on careful ordering of function calls.
  • The client has to individually save Token and Config to their persistent storage, and then restore them individually using the two restore functions, but still ensure the associated Token and Config are restored together.

Additional context

See discussion about this topic on #881.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs investigationStatus unclear, requires more work and discussion

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions