TL;DR: if you're developing a Power Pages SPA locally with local authentication (not Entra ID) and the login redirects to Microsoft with a
redirect_uripointing to the production URL, the issue isn't in your application configuration. It's the portal's "Private" flag in the admin center. Switching it to "Public" unblocks the flow.
π Background: Power Pages SPA
For those who haven't come across it yet, Power Pages Code Sites is the "code-first" mode of Power Pages portals: it lets you build the portal as a Single Page Application (React, Vue, or other frameworks), instead of managing its content through Liquid and the Dataverse admin interface.
One of the main advantages of this approach is the local developer experience: you spin up the dev server (typically Vite, on localhost:5173), and the portal behaves as it would in production, talking to the remote backend of your development environment. This way, you don't need to push every change to verify it works.
At least in theory.
π The constraint: local authentication
In its official documentation, Microsoft advises against using local authentication on Power Pages portals, recommending external providers such as Entra ID or other configurable identity providers.
In my case, however, client constraints required local authentication: no Entra ID for end users, no social login, just locally managed usernames and passwords.
This is where the first hurdle shows up: all the documentation and examples available online for Power Pages SPA authentication assume Entra ID. Searching for "Power Pages Code Sites authentication" returns guides on configuring Entra app registrations, handling JWT tokens, and silent token renewal. For local auth, you get a few lines confirming that it's supported, with no further detail.
The missing documentation can be reconstructed by reading the output generated by the published portal and analyzing its behavior. It's extra work, but it's doable.
π§± The authentication stack
To give you a concrete reference, here's β in simplified form β the React context that handles authentication on the client side. The logic is fairly straightforward: a reducer for the auth state, a fetch helper that first tries the user object injected by Power Pages into window and then falls back to a REST endpoint, and login/logout methods that redirect to the portal's native endpoints.
// AuthContext.tsx (simplified)
interface PowerPagesUser {
userName?: string;
firstName?: string;
lastName?: string;
email?: string;
contactId?: string;
}
declare global {
interface Window {
Microsoft?: {
Dynamic365?: {
Portal?: {
User?: PowerPagesUser;
};
};
};
}
}
async function fetchUser(): Promise<AuthUser | null> {
// 1. Window object (production, page-load check)
const ppUser = window.Microsoft?.Dynamic365?.Portal?.User;
if (ppUser?.userName) {
return {
id: ppUser.contactId ?? '',
email: ppUser.email ?? ppUser.userName,
fullName: `${ppUser.firstName ?? ''} ${ppUser.lastName ?? ''}`.trim(),
contactId: ppUser.contactId ?? '',
};
}
// 2. Fetch fallback (post-login session check / local dev)
const res = await fetch('/_services/auth/user');
if (!res.ok) return null;
// ... parse the JSON response or the HTML with embedded data ...
}
And the login/logout methods:
const login = useCallback(async () => {
window.location.href = '/Account/SignIn';
}, []);
const logout = useCallback(async () => {
window.location.href = '/Account/Login/LogOff?returnUrl=%2F';
}, []);
Everything is delegated to the endpoints Power Pages exposes natively: /Account/SignIn is the portal's login page, /Account/Login/LogOff is the logout endpoint. On the SPA side, after login the user is returned to the site, the page reloads, and the fetch helper retrieves the user from the window object.
In production, all of this works correctly. On the portal's public URL, the login button opens the native page, credentials are validated, and the session is established as expected.
π The problem: the wrong redirect_uri
Locally, things were different.
The scenario was as follows:
- I was working on a development portal configured as "Private" in the Power Pages admin center. Private means the portal is only visible to Entra ID users explicitly assigned as "portal users" in the admin center: a sensible choice during development, to prevent unintended access to a work-in-progress site.
- On the same portal, Entra ID authentication for end users was disabled: portal users authenticate via local auth.
- Running
npm run dev, the portal starts onlocalhost:5173. When you click the login button, however, the local portal login page doesn't open β instead, the browser is redirected tologin.microsoftonline.com.
The behavior, in itself, makes sense: the portal is "Private", so before any page can be reached β including the local login page β Power Pages requires the developer to authenticate via Entra, to verify that they are authorized to view the portal.
The problem lies in the redirect_uri of the request to Microsoft:
https://login.microsoftonline.com/.../oauth2/v2.0/authorize
?client_id=...
&redirect_uri=https%3A%2F%2Fmy-dev-portal.powerappsportals.com%2F...
&response_type=code
&scope=...
That redirect_uri points to the portal's public URL, not to http://localhost:5173. As a result, once the Entra authentication flow completes, Microsoft redirects the browser to the portal's public URL β the one configured in the underlying app registration β and the local flow breaks: the dev server is on localhost, the local code is no longer running, and the session built locally is lost.
The practical outcome is that the login cannot be completed locally, and therefore no part of the application that requires authentication can be tested locally without first deploying to the published portal π΅π.
π The search for a solution
Looking for a solution online, I didn't find much useful material.
Googling for combinations like "Power Pages Code Sites local development authentication", "Power Pages SPA private site redirect_uri localhost", or "Power Pages Vite dev server login loop" returns always the official documentation on configuring Entra authentication for portal end users, which is a different scenario from the gate for private portals.
Asking an AI assistant didn't help either, which is worth mentioning.
These days, when the documentation falls short, the natural next step is to turn to a coding assistant β and for most problems it works. Not here: Power Pages Code Sites is recent enough, and this particular combination of settings is niche enough, that there's barely any signal in the training data. The answers I got back were either generic guidance on Entra app registrations (the same material I'd already found in the docs), or plausible-sounding but ultimately wrong suggestions β the kind of confident answer that wastes more time than it saves, because you end up verifying it before discarding it. When a technology is both new and poorly documented, AI assistants reflect that gap rather than filling it.
I tried several approaches manually: checking whether localhost:5173 could be added as a valid redirect URI in the app registration used by Power Pages for the private-portal gate (it cannot, because that app registration is managed internally and isn't exposed to the tenant administrator); tracing where that redirect_uri was generated by inspecting the client-side code; setting session cookies manually; calling the auth endpoints with custom headers.
None of these led anywhere. The redirect_uri of the private-portal gate is effectively bound to the portal's production URL, and cannot be overridden from the client.
β The solution
After spending a fair amount of time exploring alternatives, the solution turned out to be a single change in the Power Pages admin center:
Power Pages admin center β Site Details β switch the flag from "Private" to "Public".
That's all. By disabling the Entra gate at the portal level:
- The local dev server can reach the portal's endpoints without first having to clear the Entra login.
-
/Account/SignInopens the portal's local auth page directly. - The login completes, the session is established, the fetch helper retrieves the user, and local development is operational again.
The reason this works is that the "Private/Public" flag has no role in end-user authentication, which remains handled by the configured providers (in my case, local auth). "Private/Public" controls the outer gate that Power Pages places in front of the portal and that requires Entra authentication only to access the portal itself. Disabling it removes the second authentication layer that, locally, was running into the wrong redirect_uri.
π A note on the documentation
It's worth making one observation about the documentation. The "Private" flag is described as a feature intended for the development phase, useful for protecting a work-in-progress portal from unintended access. In practice, however, it is incompatible with one of the main development patterns of Code Sites mode, namely working locally with the dev server.
This incompatibility isn't documented: there is no warning, footnote, or FAQ flagging it. The problem is also specific:
- With Entra ID also configured for end users, the "Private" flag causes no issues locally, because the gate flow and the application authentication flow converge and the redirect is handled consistently.
- It is only in the combination "Private" + "local auth for end users" that the flow breaks. And this is precisely the combination least covered by the official documentation.
βοΈ Operational notes
A few practical notes for anyone in the same situation:
1. First and above a all, avoid local auth whenever possible
2. Document the constraint in the repository. It's worth adding a LOCAL_DEV.md file or a README section that clearly states: "To develop locally, the Power Pages portal must be set to 'Public' in the admin center. Switching it back to 'Private' prevents login from completing locally." It's information that will save time for future developers on the project.
π§ Conclusions
The "local auth + local development" scenario on Power Pages Code Sites is one of the least documented, but it's realistic in many enterprise contexts β particularly when existing identity management systems are already in place and can't be replaced by Entra ID for end users. I hope this article is useful to anyone walking the same path, especially to those searching for an answer online after a few unproductive hours of troubleshooting.
Top comments (0)