Dataverse + SvelteKit authentication flow
Secure OAuth2 authentication with Microsoft Entra ID using PKCE, authenticating users as Dataverse SystemUsers with automatic token management and built-in security protections.
Photo by Miłosz Klinowski
Overview
Unlike Power Pages which authenticates external users as Contacts, this application authenticates internal users as Dataverse SystemUsers. Users must have a SystemUser record in Dataverse with their Azure AD Object ID matching the JWT oid claim. This provides full Dataverse permissions based on security roles rather than portal-specific web roles.
The authentication flow uses Microsoft’s MSAL Node library with PKCE for enhanced security. When a user clicks login, the application generates a random PKCE verifier and its SHA-256 hash (challenge), stores the verifier in an httpOnly cookie, and redirects to Azure AD with the challenge. After the user authenticates and consents to Dataverse access, Azure AD redirects back with an authorization code.
The callback handler validates the CSRF token from the state parameter, exchanges the authorization code plus PKCE verifier for an access token, and stores it in an httpOnly cookie. The PKCE flow ensures that even if an attacker intercepts the authorization code, they cannot exchange it without the verifier.
Request authentication
Every request passes through the handle hook in hooks.server.ts, which calls authenticateUser() to validate the token. The function decodes the JWT, checks if it’s expired, and either returns a cached user from cookies (10-minute TTL) or fetches the SystemUser from Dataverse by matching the JWT’s oid claim to the azureactivedirectoryobjectid field:
const systemUser = await fetchSingle<SystemUser>(
'systemusers',
`azureactivedirectoryobjectid=${accessToken.oid}`
).select([
'systemuserid, domainname, azureactivedirectoryobjectid',
'firstname, lastname, fullname, title, mobilephone',
'address1_line1, address1_postalcode, address1_city, address1_country'
])
// .expand('system_user_contacts($select=contactid,firstname,lastname)') OData metadata is stripped before caching to reduce cookie size. The user object is then available via locals.user in all server load functions and as reactive state in components via the getUserState() context API.
Automatic API authentication
The handleFetch hook automatically injects the Bearer token into all Dataverse API requests, along with required OData headers. This means you never manually manage authentication headers in your code—just make requests and the token is added automatically:
if (request.url.startsWith(PUBLIC_DYNAMICS_API_URL) && accessToken) {
request.headers.set('Authorization', `Bearer ${accessToken}`)
request.headers.set('OData-Version', '4.0')
request.headers.set('Content-Type', 'application/json')
request.headers.set('Prefer', 'odata.include-annotations=*')
} Security protections
The implementation uses defense-in-depth security with multiple layers:
PKCE prevents authorization code interception by requiring the attacker to have both the code and the secret verifier stored server-side. CSRF protection uses a random token stored in httpOnly cookies and validated against the OAuth state parameter. HttpOnly cookies prevent JavaScript from accessing tokens, protecting against XSS attacks. SameSite=Lax cookies aren’t sent with cross-site POST requests, preventing CSRF attacks.
Token expiration is validated on every request—expired tokens trigger immediate re-authentication with the original URL preserved for post-login redirect. The user cache reduces Dataverse API calls by 90% while automatically invalidating when tokens expire.
User provisioning
This application does not support self-registration. Users must be provisioned as SystemUsers by administrators through the Microsoft 365 Admin Center, Power Platform Admin Center, and Dataverse security role assignment. SystemUsers are full Dataverse application users with licenses, business unit assignments, and security roles—they cannot be self-provisioned like Contact records in Power Pages.
If a user authenticates successfully with Azure AD but no matching SystemUser exists in Dataverse, the fetchSingle call returns a 404 error and the user sees an error page. There is no automatic Contact creation or registration flow.
Environment variables
Public variables (PUBLIC_BASE_URL, PUBLIC_DYNAMICS_BASE_URL, PUBLIC_DYNAMICS_API_URL) are accessible in both client and server code. Private variables (CLOUD_INSTANCE, TENANT_ID, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI) are server-only and never sent to the browser.
The redirect URI must exactly match the value configured in the Azure AD app registration, including protocol (http/https) and port. Mismatches cause CSRF token validation failures.
Logout
Calling /api/auth/logout deletes the accessToken and user cookies, then redirects to Azure AD’s logout endpoint with a post_logout_redirect_uri parameter. This clears both the application session and the Azure AD SSO session, ensuring complete sign-out. The user is redirected back to the application homepage with no authentication state.
| Feature | Power Pages | This Application |
|---|---|---|
| User Entity | Contact + External Identity | SystemUser |
| Provisioning | Self-registration | Admin provisioning |
| Permissions | Web Roles (portal-specific) | Security Roles (full Dataverse) |
| OAuth Flow | Authorization Code | Authorization Code + PKCE |
| Identity Match | adx_username (email) | azureactivedirectoryobjectid (Object ID) |
| License | No license required | Dataverse user license required |
| Use Case | External users, customers | Internal users, employees |
Resources
- Microsoft Authentication Library for Node.js (MSAL Node)
- OAuth 2.0 Authorization Code Flow
- PKCE: Proof Key for Code Exchange
- Microsoft Entra ID Authentication Overview
- Dataverse Web API Authentication
- SvelteKit Hooks Documentation
- OWASP Security Cheat Sheets
This authentication implementation provides a secure, production-ready foundation for authenticating internal users as Dataverse SystemUsers. By leveraging PKCE, httpOnly cookies, CSRF protection, and automatic token management through SvelteKit hooks, the system defends against common web security threats while maintaining a seamless user experience. The key distinction from Power Pages—authenticating as SystemUsers with full Dataverse permissions rather than Contacts with portal-specific roles—makes this approach ideal for building internal business applications that require comprehensive access to organizational data based on security role assignments.