Skip to content

Add support for custom redirect URI paths #14

@koistya

Description

@koistya

Description

Currently, the library defaults to /callback as the redirect path. Many OAuth providers require specific redirect URIs to be registered exactly, and developers may need to use different paths. Add support for custom and multiple callback paths.

What needs to be done

  1. Allow custom callback paths in configuration:

    • Single custom path
    • Multiple paths for different providers
    • Path pattern matching (wildcards, regex)
  2. Update server implementations to handle:

    • Multiple registered paths
    • Path validation
    • Path-specific handlers
  3. Enhance documentation to explain:

    • How to configure custom paths
    • Provider-specific requirements
    • Security considerations

Why this matters

Redirect URI flexibility is essential:

  • Provider requirements: Some providers enforce specific paths
  • Multi-tenant apps: Different paths for different clients
  • Legacy support: Maintaining backward compatibility
  • Security: Path-based access control

Current limitations:

  • Can't use library with providers requiring specific paths
  • Can't handle multiple OAuth flows simultaneously
  • No support for path-based routing

Implementation considerations

⚠️ Note: This feature requires critical thinking during implementation. Consider:

  1. Security: How do we prevent redirect URI manipulation attacks? Should we whitelist paths?

  2. Path matching: Exact match vs pattern matching? What about query parameters in the path?

  3. Alternative approach: Instead of multiple paths, use a single path with provider identification in state parameter

  4. Backward compatibility: How do we maintain compatibility with existing /callback users?

  5. Provider detection: Should we auto-detect the provider from the callback path?

Suggested implementation

// Option 1: Simple custom path
interface GetAuthCodeOptions {
  // ... existing options
  callbackPath?: string; // Already exists but needs enhancement
  callbackPaths?: string[]; // New: multiple paths
  pathMatcher?: (path: string) => boolean; // New: custom matcher
}

// Option 2: Advanced path configuration
interface PathConfig {
  path: string;
  exact?: boolean; // Exact match vs prefix
  provider?: string; // Associate with provider
  handler?: (params: CallbackResult) => CallbackResult; // Custom handler
}

interface GetAuthCodeOptions {
  // ... existing options
  paths?: PathConfig[];
}

// Implementation in server
class CallbackServer {
  private paths: Map<string, PathConfig> = new Map();
  
  registerPath(config: PathConfig): void {
    this.paths.set(config.path, config);
  }
  
  private handleRequest(request: Request): Response {
    const url = new URL(request.url);
    const pathname = url.pathname;
    
    // Try exact match first
    let config = this.paths.get(pathname);
    
    // Try prefix match if no exact match
    if (!config) {
      for (const [path, pathConfig] of this.paths) {
        if (!pathConfig.exact && pathname.startsWith(path)) {
          config = pathConfig;
          break;
        }
      }
    }
    
    // Try custom matcher
    if (!config) {
      for (const [, pathConfig] of this.paths) {
        if (pathConfig.matcher?.(pathname)) {
          config = pathConfig;
          break;
        }
      }
    }
    
    if (!config) {
      return new Response("Not Found", { status: 404 });
    }
    
    // Handle OAuth callback
    const params = this.extractParams(url);
    
    // Apply custom handler if provided
    const result = config.handler ? config.handler(params) : params;
    
    // Resolve the appropriate promise based on path
    this.resolveCallback(config.path, result);
    
    return new Response(/* success/error HTML */);
  }
}

// Usage examples

// 1. Simple custom path
await getAuthCode({
  authorizationUrl: "...",
  callbackPath: "/auth/github/callback"
});

// 2. Multiple paths for different providers
const server = createCallbackServer();
await server.start({ port: 3000 });

// GitHub flow
const githubPromise = server.waitForCallback("/auth/github/callback", 30000);

// Google flow (concurrent)
const googlePromise = server.waitForCallback("/auth/google/callback", 30000);

// 3. Advanced configuration with patterns
await getAuthCode({
  authorizationUrl: "...",
  paths: [
    {
      path: "/auth/github/callback",
      exact: true,
      provider: "github"
    },
    {
      path: "/auth/",
      exact: false, // Prefix match for /auth/*
      handler: (params) => {
        // Custom processing
        console.log("Auth callback:", params);
        return params;
      }
    },
    {
      matcher: (path) => path.match(/^\/oauth\/\d+\/callback$/),
      provider: "dynamic"
    }
  ]
});

// 4. Backward compatible (default behavior)
await getAuthCode("https://..."); // Uses /callback

// 5. Provider-specific paths
const providers = {
  github: "/auth/github/return",
  google: "/signin-oidc",
  microsoft: "/auth/microsoft/callback",
  auth0: "/callback",
  okta: "/authorization-code/callback"
};

await getAuthCode({
  authorizationUrl: "...",
  callbackPath: providers[selectedProvider]
});

Path validation requirements

function validateCallbackPath(path: string): void {
  // Must start with /
  if (!path.startsWith('/')) {
    throw new Error("Callback path must start with /");
  }
  
  // No double slashes
  if (path.includes('//')) {
    throw new Error("Callback path cannot contain //");
  }
  
  // No query parameters
  if (path.includes('?')) {
    throw new Error("Callback path cannot contain query parameters");
  }
  
  // No fragments
  if (path.includes('#')) {
    throw new Error("Callback path cannot contain fragments");
  }
  
  // Reasonable length
  if (path.length > 256) {
    throw new Error("Callback path too long");
  }
  
  // No path traversal
  if (path.includes('../')) {
    throw new Error("Callback path cannot contain ../");
  }
}

Testing requirements

  • Test single custom path
  • Test multiple concurrent paths
  • Test path pattern matching
  • Test path validation
  • Test backward compatibility
  • Test security (path traversal attempts)
  • Test with real provider requirements

Documentation updates

Update README with:

  • Custom path examples
  • Provider-specific path requirements
  • Security best practices for paths
  • Migration guide from default path

Skills required

  • TypeScript
  • HTTP routing concepts
  • URL path handling
  • Security awareness
  • API design

Difficulty

Easy - Straightforward enhancement with important design decisions

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions