8000 Add getOAuthHelpers method by DeanMauro · Pull Request #117 · cloudflare/workers-oauth-provider · GitHub
[go: up one dir, main page]

Skip to content

Conversation

@DeanMauro
Copy link
Contributor
@DeanMauro DeanMauro commented Dec 12, 2025

This PR makes OAuthHelpers available outside of the fetch method (i.e. in a worker's RPC methods).

Problem

image

this.env.OAUTH_PROVIDER gets populated in OAuthProviderImpl.fetch(), so if we try to use it in a worker's RPC methods, for example, it will always be undefined.

const oauthProvider = new OAuthProvider({});

export default class extends WorkerEntrypoint<AuthWorkerEnv> {
  async fetch(request: Request) {...}

  async checkClient(id: string) {
    // ERROR because this.env.OAUTH_PROVIDER isn't set yet
    const existingClient = await this.env.OAUTH_PROVIDER.lookupClient(id);

    // SUCCESS
    const existingClient = await oauthProvider.getOAuthHelpers(this.env).lookupClient(id);
  }
}

Solution

We need a method OAuthProvider.getOAuthHelpers(env: any) that initializes and retrieves OAuthHelpers directly from the provider.

❌ We can't put the method directly onto the provider because it will get exposed, intentionally or otherwise, as a public RPC method on the worker.

export class OAuthProvider {
  ...
 getOAuthHelpers(env: any) {
      return this.#impl.getOAuthHelpers(env);
  };
}

✅ But if we use a Symbol to name the method, it appears on the provider like we want, but excluded from the worker's RPC methods.

export class OAuthProvider {
  ...
  [getOAuthHelpersSymbol](env: any): OAuthHelpers {
    return this.#impl.getOAuthHelpers(env);
  }
}

export function getOAuthHelpers(provider: OAuthProvider, env: any): OAuthHelpers {
  return provider[getOAuthHelpersSymbol](env);
}

@changeset-bot
Copy link
changeset-bot bot commented Dec 12, 2025

⚠️ No Changeset found

Latest commit: d289be5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@DeanMauro
Copy link
Contributor Author

@mattzcarey - Can you give this a review?

@kentonv - Found a neat way to add methods to the provider without exposing them over RPC

@DeanMauro DeanMauro requested a review from kentonv December 13, 2025 03:02
* @param env - Cloudflare Worker environment variables
* @returns An instance of OAuthHelpers
*/
export function getOAuthHelpers(provider: OAuthProvider, env: any): OAuthHelpers {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, so, this API works (and it's what I asked for), but it still feels weird to me.

I wonder if it should be something like:

function getInternalApi(options: OAuthProviderOptions, storage: KvNamespace): OAuthHelpers;

That is, you don't pass it an existing provider instance, nor env... you just pass in the options and storage namespace, and it makes a new one. (This might require making a hidden OAuthProviderImpl instance under the hood, but that's OK.)

Feels a little cleaner as a library function. OAuthProvider has a sort of weird design since it's specifically designed to be a wrapper around your whole app. But the point of this function is for when you don't want OAuthProvider wrapping your whole app...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno, what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud, we can frame the elements this way:

OAuthProviderImpl: Core OAuth toolset
OAuthHelpers/OAuthHelpersImpl: Take in OAuthProviderImpl and returns a subset of features publicly
OAuthProvider: Takes in OAuthProviderOptions, creates OAuthProviderImpl, and returns OAuthHelpers+ a routing layer.
New element: Takes in OAuthProviderOptions, creates OAuthProviderImpl and returns OAuthHelpers.

Yeah it's a messy task because on one hand, the last two elements do nearly the same thing, but on the other, OAuthProvider's primary purpose could be seen as routing and reusing it does mix concerns. Then again, OAuthProviderOptions deals mostly with routing so we're mixing concerns either way.

Copy link
Contributor Author
@DeanMauro DeanMauro Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how I currently use it:

// Create the OAuth provider instance
const oauthProvider = new OAuthProvider({
  apiHandlers: {},
  defaultHandler: defaultHandler,
  authorizeEndpoint: '/authorize',
  tokenEndpoint: '/token',
  clientRegistrationEndpoint: '/register',
  tokenExchangeCallback: tokenExchangeCallback,
  scopesSupported: AllScopes,
  allowImplicitFlow: false,
  allowTokenExchangeGrant: false
});

export default class extends WorkerEntrypoint<AuthWorkerEnv> {
  async fetch(request: Request) {
    // ... other code ...
    return oauthProvider.fetch(request, this.env, this.ctx);
  }

  /** Manually register a client (RPC) */
  public async upsertClient(data: OAuthClientInfo, clientType: ClientType): Promise<OAuthClient> {
    const oauthHelpers = getOAuthHelpers(oauthProvider, this.env);
    return await oauthHelpers.lookupClient(data.clientId ?? '');
  }
}

This is the alternative:

const oauthOptions = {
  apiHandlers: {},
  defaultHandler: defaultHandler,
  authorizeEndpoint: '/authorize',
  tokenEndpoint: '/token',
  clientRegistrationEndpoint: '/register',
  tokenExchangeCallback: tokenExchangeCallback,
  scopesSupported: AllScopes,
  allowImplicitFlow: false,
  allowTokenExchangeGrant: false
};

// Create the OAuth provider instance
const oauthProvider = new OAuthProvider(oauthOptions);

export default class extends WorkerEntrypoint<AuthWorkerEnv> {
  async fetch(request: Request) {
    // ... other code ...
    return oauthProvider.fetch(request, this.env, this.ctx);
  }

  /** Manually register a client (RPC) */
  public async upsertClient(data: OAuthClientInfo, clientType: ClientType): Promise<OAuthClient> {
    const oauthHelpers = getOAuthHelpers(oauthOptions, this.env);
    return await oauthHelpers.lookupClient(data.clientId ?? '');
  }
}

Copy link
Contributor Author
@DeanMauro DeanMauro Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with the alternative (sharing options), but would keep the signature as (options, env) since env is used everywhere down the line. Can always add a KVNamespace to OAuthProviderOptions if we want to make it configurable later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm well the env part is actually the part that bugs me more, it doesn't feel like you should have to give it your whole env. But I'm always the guy who is weirdly protective of my env in a way nobody else seems to care about.

At the very least though I don't think this function should inject OAUTH_PROVIDER into env... that seems unnecessary, given the caller already has a reference to the provider and can do it themselves if they really want to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll put the OAUTH_PROVIDER line back into the fetch method where it came from. Right, no one will be using that_and_ this method.

I wouldn't remove env though. Between OAuthProviderImpl.fetch(req, env, ctx) and import { env } from "cloudflare:workers", it's getting in there anyway. Even in RPC methods, you can still

  public async bypass() {
    // Dummy call to initialize the OAuth provider
    await this.fetch(new Request('https://test.com/token', { method: 'GET' }));

    // Now we can use the OAuth provider with env in it
    this.env.OAUTH_PROVIDER.lookupClient('test');
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit busy today and tomorrow but will implement any changes later this week!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0