Skip to content

[Feature]: Add callbackPath to Support OAuth/OIDC Redirects #1790

@gugupy

Description

@gugupy

Description

Summary

Add a callbackPath option to buildAuthenticatedRouter (and equivalent adapters) to support OAuth 2.0 / OpenID Connect login redirects.

Problem

Currently, handleLogin() is only triggered on loginPath (usually via POST), which works for form-based authentication.

However, OAuth/OIDC providers (e.g. Azure AD, Keycloak, Auth0, Okta) redirect back via GET with authorization code and state:

GET /admin/auth/callback?code=xyz&state=abc

Since AdminJS doesn’t expose a built-in GET route for this, developers must manually create custom routes, handle code exchange, manage sessions, and perform redirects — duplicating logic that the plugin could handle natively.

Suggested Solution

Extend all authentication adapters (@adminjs/express, @adminjs/fastify, @adminjs/koa, @adminjs/hapi) to support an optional callbackPath:

const admin = new AdminJS({
  componentLoader,
  rootPath: '/admin',
  callbackPath: '/admin/oauth/callback',
});

buildAuthenticatedRouter(admin, {
  provider: myOAuthProvider,
});

The adapter should register a GET route that invokes handleLogin() with the query parameters:

router.get(callbackPath, async (req, res) => {
  const currentAdmin = await authProvider.handleLogin({ query: req.query }, { req, res });

  if (currentAdmin) {
    req.session.adminUser = currentAdmin;
    return res.redirect(admin.options.rootPath);
  }

  res.redirect(`${loginPath}?error=oauth_failed`);
});

Benefits

  • Native OAuth 2.0 / OIDC flow support
  • Zero custom routing boilerplate
  • Works seamlessly with Keycloak, Azure AD, Auth0, Okta, etc.
  • Maintains backward compatibility
  • Keeps session handling within AdminJS

Alternatives

Add a custom middleware to handle OAuth callback (GET request with code)

app.get(admin.options.rootPath + '/login', async (req, res, next) => {
  
  // Check if this is a callback with authorization code
  if (req.query.code) {
    
    // Instead of handling authentication manually, create a form that POSTs to AdminJS
    // This way AdminJS handles the session properly
    const html = `
      <!DOCTYPE html>
      <html>
      <head>
        <title>Processing Login...</title>
      </head>
      <body>
        <div style="text-align: center; margin-top: 50px;">
          <h2>Processing your login...</h2>
          <p>Please wait while we complete your authentication.</p>
        </div>
        <form id="loginForm" method="POST" action="${admin.options.rootPath}/login" style="display: none;">
          <input type="hidden" name="code" value="${req.query.code}" />
          <input type="hidden" name="redirectUri" value="http://localhost:3000${admin.options.rootPath}/login" />
        </form>
        <script>
          document.getElementById('loginForm').submit();
        </script>
      </body>
      </html>
    `;
    
    return res.send(html);
  }
  
  // If no code, continue to the normal AdminJS login flow
  next();
});

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions