Silent authentication: prompt=none issue

What is the issue

In Election Financial Filing System application using ForgeRock JavaScript SDK, when a user clicks login button and the URL is changed to http://localhost:9081/?error_description=The%20request%20requires%20some%20interaction%20that%20is%20not%20allowed.&state=MTU5NTcxNDMxOTMyNTg2MTkxMjEyMTkxOTMxMzM3MjUwNTEyMTkyMjQ&error=interaction_required#login and redirected to the error page. Noticed that this error is not happening with oidc-client-ts library but ForgeRock JavaScript SDK.

Expected behavior

In the normal flow, when a user clicks a login button, it is supposed to show the ForgeRock login page so that a user enters credentials. After successfully login, it should be redirected back to the post-login page.

Observations

The issue is occurring only in the Election system. The same cotjs-sdk is used for MLS, where it works fine. So, I investigated the issue by comparing the two systems.

  1. MLS checks authorization and authentication and render the page at the root route but Election switches to #login route and render login page. There is a “Create an Account” link which redirects to the registration journey flow.

image-20250416-034639.png
  1. When login button is clicked, MLS and Election system both make a call ForgeRck /authorize endpoint. https://openam-torontopda-nane1-dev.id.forgerock.io/am/oauth2/bravo/authorize?client_id=MCCAClient-SIT&redirect_uri=http%3A%2F%2Flocalhost%3A9081&response_type=code&scope=openid%20profile&state=xxxxx&prompt=none&code_challenge=yyyyy&code_challenge_method=S256

  2. In the Election system, when it switches to the error page, clicking the browser's back button successfully redirects to the ForgeRock login page.

Diagnose and Fix

Observation #3 made me suspicious of the routing in the Election system. I found that the route definition is not intuitive but rather complex. The back button resolving the issue suggests that the router or app state is misaligned during the callback, or it could indicate that the SDK’s redirect URL is queued.

To address this, I created a simplified AppRouter and integrated the cotjs-sdk for authentication. With the simplified router, the error was resolved, and the login process was successfully completed.

// main.js (async function () { window.App = { views: {}, router: null, }; $("#appDisplay").removeClass("hide"); $("#app-breadcrumb").hide(); $("#header-right").remove(); const settings = { clientId: "MCCAClient-SIT", redirectUri: "http://localhost:9081", scope: "openid profile", baseUrl: "https://openam-torontopda-nane1-dev.id.forgerock.io/am/oauth2/bravo/.well-known/openid-configuration", timeout: 3000, realmPath: "bravo", tokenStore: "sessionStorage", tree: "COT-LoginUser", }; effs.mgr = cotjsSDK.auth(settings); cotjsSDK.setStore("authInstance", effs.mgr); await effs.mgr.configSDK(); // Initialize router App.router = new AppRouter(); // Start hash-based routing Backbone.history.start(); console.log("==> main history start"); })();
// AppRouter.js (function () { var AppRouter = Backbone.Router.extend({ routes: { "": "home", dashboard: "dashboard", }, initialize: function () { this.currentView = null; }, home: function () { this.renderView(new HomeView()); }, dashboard: function () { this.renderView(new DashboardView()); }, renderView: function (view) { if (this.currentView) { this.currentView.remove(); } this.currentView = view; $("#effs-container .main-content").html(this.currentView.render().el); }, }); window.AppRouter = AppRouter; })();
// homeView.js (function () { var HomeView = Backbone.View.extend({ tagName: "div", template: _.template( "<% if (isLoggedIn) { %>" + "<h1>Election System!</h1>" + "<p>You are logged in. <button id='pda-logout-btn'>logout</button></p>" + "<% } else { %>" + "<h1>Welcome to the Election Home Page</h1>" + '<p>Please <button id="pda-login-btn">log in</button> to continue.</p>' + "<% } %>" + "<% if (error) { %>" + '<p class="error"><%= error %></p>' + "<% } %>" ), events: { "click #pda-login-btn": "loginHandler", "click #pda-logout-btn": "logoutHandler", }, initialize: async function () { this.model = new Backbone.Model({ isLoggedIn: false, error: null, }); this.listenTo(this.model, "change", this.render); await this.checkAuth(); }, loginHandler: function () { cotjsSDK.getStore("authInstance").login(); }, logoutHandler: async function () { await effs.mgr.logout(); //cotjsSDK.getStore("authInstance").logout(); this.model.set({ isLoggedIn: false }); }, render: function () { this.$el.html(this.template(this.model.toJSON())); return this; }, checkAuth: async function () { const authInfo = await cotjsSDK.getStore("authInstance").authorize(); const tokens = authInfo ? authInfo.tokens : null; if (tokens) { cotjsSDK.setStore("tokens", tokens); this.model.set({ isLoggedIn: true }); } else { const authed = await cotjsSDK .getStore("authInstance") .isAuthenticated(); if (authed) { const tk = await cotjsSDK.getStore("authInstance").getTokens(); cotjsSDK.setStore("isAuthed", true); cotjsSDK.setStore("tokens", tk); this.model.set({ isLoggedIn: true }); } else { this.model.set({ isLoggedIn: false }); } } }, remove: function () { this.$el.remove(); this.stopListening(); return this; }, }); // Expose globally window.HomeView = HomeView; })();

Silent Authentication: prompt=none parameter

As mentioned in observation #2, the prompt=none parameter is included in the authorize endpoint. Initially, I suspected it was causing the error. However, after resolving the issue with a simplified router, I reconsidered whether this was truly the problem.

In an SSO environment, silent authentication is the correct approach. With SSO, a user can log in to different systems. When a user lands on a page, the system should first check if they are already logged in. If they are, it should 'silently' guide them to the post-login page, eliminating the need to click a login button again.

From this perspective, using the authorize endpoint with prompt=none is appropriate. The issue is that in MLS, the system silently attempts authentication a couple of times and, if the user is not logged in, redirects to the login page. In contrast, the Election system switches directly to the error page.

Silent authentication between MLS with ForgeRock SDK and Election system with oidc-client-ts

In MLS, after logging in on one tab, opening a new tab and accessing the landing page does not display the login button again; it directly routes you to the post-login page. In contrast, in the Election system, when you open a new tab and access the system, the login button is displayed again. Clicking it skips the login page and directs you to the post-login page.

This suggests that the Election system does not provide true “silent authentication”.

Final thoughts / recommendations

  • The AppRouter for the Election system needs to be refactored to handle authorization at the root route.

  • To achieve true silent authentication, the OAuth wrapper should be refactored to utilize the silent authentication feature of oidc-client.

  • For the cotjs-sdk, it may be worthwhile to add a module to support integration with oidc-client-ts.

Related content