adminjs icon indicating copy to clipboard operation
adminjs copied to clipboard

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

Open gugupy opened this issue 3 months ago • 0 comments

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

gugupy avatar Oct 18 '25 20:10 gugupy