Architecture: what you’re actually deploying
You’re building a customer‑facing website using Power Pages backed by Dataverse for data and access control, while Entra External ID handles customer authentication. The architecture has four layers:
Tenant & environments. Use a production tenant with at least three Dataverse environments (Dev, Test, Prod). Keep each Power Pages site bound to a single environment. Store tenant‑specific identifiers (B2C authority, policy names, client ID) in Environment Variables so you don’t hard‑code URIs.
Identity. Entra External ID issues tokens via user flows (recommended for most) or custom policies (needed for advanced claims orchestration). The portal uses OpenID Connect; the customer identity is linked to a Dataverse Contact row through an External Identity record.
Data. Dataverse hosts tables (e.g., Case, Order, Profile). Table Permissions plus Web Roles enforce per‑user access. The portal renders forms and lists from Dataverse; you augment with Power Fx logic and server‑side plugins where needed.
Edges. Place Azure Front Door or your preferred CDN/WAF in front of Power Pages for TLS, caching, bot mitigation, IP throttling, and geo‑routing. Stream diagnostics to Application Insights and export Entra sign‑in logs to a Log Analytics workspace.
Identity flows that feel native
Sign‑up and sign‑in. Start with a single combined flow. In Entra External ID, create a Sign up and sign in user flow and add the claims you need (givenName, surname, email). If you’re on custom policies, name them clearly: B2C_1A_SIGNUP_SIGNIN, B2C_1A_PASSWORDRESET, B2C_1A_PROFILEEDIT.
Password reset. Use a dedicated flow reachable from the sign‑in page; in custom policies, add a SelfAsserted-PasswordReset journey, or in user flows just enable Password reset and capture the endpoint.
MFA. Enable MFA in the user flow (SMS, email OTP, or authenticator app). For higher assurance at specific actions (e.g., “submit order”), require Reauthenticate by redirecting to the same policy with the prompt=login hint.
Branding & attributes. Upload your logo, colors, and terms of use in the user flow, and add custom attributes such as marketingConsent or customerNumber. Map those attributes into Dataverse on first sign‑in.
Sample custom policy fragment (MFA enforced):
<!-- Relying party using combined sign-up/sign-in with MFA -->
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignInMFA" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>JWT Issuance</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="extension_customerNumber" PartnerClaimType="customerNumber" />
<OutputClaim ClaimTypeReferenceId="amr" />
</OutputClaims>
</TechnicalProfile>
</RelyingParty>
Power Pages OIDC site settings (no client secret required):
# Add as Site Settings in Power Pages
Authentication/OpenIdConnect/AzureAD/Authority: https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1A_SIGNUP_SIGNIN/v2.0
Authentication/OpenIdConnect/AzureAD/ClientId: 00000000-0000-0000-0000-000000000000
Authentication/OpenIdConnect/AzureAD/RedirectUri: https://www.contoso.com/signin-oidc
Authentication/OpenIdConnect/AzureAD/PostLogoutRedirectUri: https://www.contoso.com/
Authentication/OpenIdConnect/AzureAD/ResponseType: code
Authentication/OpenIdConnect/AzureAD/Scope: openid profile email
Data model and authorization that actually holds
Model the minimum viable set first. A typical shape:
- Contact (portal user), extended with CustomerNumber, MarketingConsent, PreferredLanguage.
- Case (or Request), owned by Contact, status, description, attachments.
- Attachment for file uploads, stored via Power Pages file column with virus scanning on your WAF.
Table Permissions enforce row‑level scope so a customer only sees their own records.
Example table permission (read/write own cases):
{
"name": "Cases - Self Access",
"table": "msdyn_case",
"privileges": ["Read", "Create", "Append", "AppendTo", "Write"],
"scope": "Contact",
"contactRelationship": "customerid",
"webRoles": ["Authenticated Users"]
}
Web Roles. Create Authenticated Users and VIP. Bind a Contact to roles via the portal’s “Website > Security > Web Roles”. VIP can see premium content or increased quotas.
Power Fx on a form to create a Case and store consent:
// OnSelect of a custom submit button on a Power Pages form component
Patch('msdyn_case', Defaults('msdyn_case'), {
title: txtTitle.Text,
description: txtDesc.Text,
customerid: LookUp(Contacts, email = User().Email),
'contoso_marketingconsent': tglConsent.Value
});
Notify("Request submitted.", NotificationType.Success);
ResetForm(EditForm1);
C# plugin to map claims on first sign‑in and assign a web role:
public class PostContactCreate : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var ctx = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
if (!ctx.InputParameters.Contains("Target")) return;
var entity = (Entity)ctx.InputParameters["Target"]; // contact
var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var svc = serviceFactory.CreateOrganizationService(ctx.UserId);
// Map B2C extension claims into contact fields when available
if (entity.Attributes.Contains("emailaddress1"))
{
var email = entity.GetAttributeValue<string>("emailaddress1");
// Example: lookup extension_customerNumber from an external cache or token hint
if (!entity.Attributes.Contains("contoso_customernumber"))
entity["contoso_customernumber"] = "PENDING";
}
// Add to 'Authenticated Users' web role
var role = svc.RetrieveMultiple(new QueryExpression("adx_webrole")
{
ColumnSet = new ColumnSet("adx_name"),
Criteria = { Conditions = { new ConditionExpression("adx_name", ConditionOperator.Equal, "Authenticated Users") } }
}).Entities.FirstOrDefault();
if (role != null && entity.Id != Guid.Empty)
{
var intersect = new Entity("adx_webrole_contact")
{
["adx_webroleid"] = role.ToEntityReference(),
["adx_contactid"] = entity.ToEntityReference()
};
svc.Create(intersect);
}
}
}
ALM that won’t break on Friday night
Build everything inside a Solution. Add your Power Pages site, tables, web roles, table permissions, model‑driven app (if any), flows, and plugins. Use Environment Variables for Authority, ClientId, policy names, and redirect URIs so Test/Prod don’t need code edits.
CLI moves:
# In Dev, download your site assets
pac powerpages download --path ./portal-src --website "Contoso Portal"
# Commit to git; use profiles for each environment
pac powerpages profile add --name dev --path ./portal-src
pac powerpages profile add --name test --path ./portal-src
# Export and import solutions
pac solution export --name ContosoPortal --path ./out --managed false
pac solution import --path ./out/ContosoPortal.zip --target test --force
# Upload portal to Test using the test profile
pac powerpages upload --path ./portal-src --profile test
Avoid unmanaged customizations in Test/Prod. Promote a managed solution to Prod after UAT. Keep Connection References for flows and configure them per environment.
Deployment checklist: set environment variables, verify Entra reply URLs, bind custom domain and TLS cert, and run smoke tests for sign‑in, create case, and attachment upload.
Security checklist you’ll actually use
Start by assuming the internet is hostile, then remove as many surprises as possible.
Edge controls. Put a WAF/CDN up front (Azure Front Door or Cloudflare). Enforce TLS 1.2+, HSTS, and turn on bot protection. Rate‑limit /signin-oidc, /signout-callback-oidc, and API endpoints that back Entity Lists.
Portal hardening. Require authentication for any page that leaks data. Disable OData feeds unless they’re essential. Keep Table Permissions on every Entity List and every form. Use CAPTCHA for anonymous submissions.
Identity hygiene. Force MFA for all high‑risk actions. Add conditional access if you run into abuse. Rotate or invalidate risky accounts by disabling the corresponding Contact.
Storage & PII. Store only what you use. Flag sensitive columns as restricted in model‑driven apps, and do not echo PII into Liquid templates.
Observability. Enable Dataverse auditing on key tables. Stream Power Pages diagnostics to Application Insights. Export Entra sign‑in logs to Log Analytics and set alerts for spikes in AADB2C90118 (password reset) or AADB2C90091 (throttling).
KQL example to watch sign‑in errors:
SigninLogs
| where AppDisplayName == "Contoso Portal"
| summarize count() by ResultType, bin(TimeGenerated, 15m)
Sample portal config you can copy
# Site Settings (excerpt)
Website/EnableCustomErrors: true
Authentication/Registration/Enabled: true
Authentication/Registration/LoginButtonAuthenticationType: OpenIdConnect-AzureAD
Authentication/OpenIdConnect/AzureAD/Authority: https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1A_SIGNUP_SIGNIN/v2.0
Authentication/OpenIdConnect/AzureAD/ClientId: 00000000-0000-0000-0000-000000000000
Authentication/OpenIdConnect/AzureAD/ResponseType: code
Portal/EnableFastSearch: false
Portal/EnableRateLimit: true
Portal/MaxRequestsPerMinute: 60
// Web roles and page access (conceptual)
{
"webRoles": [
{ "name": "Anonymous Users" },
{ "name": "Authenticated Users" },
{ "name": "VIP" }
],
"pageAccess": [
{ "page": "/cases", "roles": ["Authenticated Users", "VIP"], "authRequired": true },
{ "page": "/vip", "roles": ["VIP"], "authRequired": true }
]
}
Troubleshooting notes that save hours
If the portal loops on sign‑in, check that the Authority contains the correct tenant and policy and that RedirectUri exactly matches the portal domain. If table data appears empty, the usual culprit is missing Table Permissions or the page is anonymous. For intermittent 401s, confirm that the clock skew on your WAF is small and the policy tokens aren’t exceeding the default lifetime.