Microsoft Entra ID OAuth
Pia signs users in via OAuth — Google and Microsoft Entra ID are supported out of the box. This page walks through registering an Entra application and configuring the server.
1. Register the Entra application
Section titled “1. Register the Entra application”In the Microsoft Entra admin centre, create a new app registration:
| Setting | Value |
|---|---|
| Name | Pia Server dev (or your production name) |
| Supported account types | Any Entra ID Tenant + Personal Microsoft accounts |
| Redirect URI platform | Web |
| Redirect URI | https://<your-server-url>/signin-microsoft |
2. Configure the server
Section titled “2. Configure the server”Add the Client ID and Client Secret to appsettings.json (or as environment variables in production):
{ "OAuth": { "Microsoft": { "ClientId": "<from Entra app registration>", "ClientSecret": "<from Entra → Certificates & secrets>" } }}In production, prefer environment variables: OAUTH_MICROSOFT_CLIENT_ID and OAUTH_MICROSOFT_CLIENT_SECRET.
3. The flow
Section titled “3. The flow”The desktop app uses PKCE (S256). It starts a one-shot loopback listener, opens the browser to /auth/login?provider=microsoft&redirect_uri=http://localhost:{port}/&code_challenge=<S256>&code_challenge_method=S256&state=<nonce>, and waits for the server to redirect back with a one-time login code, never tokens. It then trades that code, together with its PKCE verifier, for tokens at POST /auth/token.
┌──────────────┐ ┌─────────────┐ ┌──────────────┐ ┌───────────┐│ WPF Client │ │ Browser │ │ Pia Server │ │ Microsoft │└──────┬───────┘ └──────┬──────┘ └──────┬───────┘ └─────┬─────┘ │ 1. Pick port, │ │ │ │ start listener, │ │ │ │ make PKCE pair │ │ │ │ │ │ │ │ 2. Open browser ──►│ │ │ │ to /auth/login │ │ │ │ + code_challenge│ │ │ │ │ 3. GET /auth/login │ │ │ │───────────────────►│ │ │ │ 4. 302 to MS │ │ │ │◄───────────────────│ │ │ │ 5. Sign in ──────────────────────────►│ │ │ │ 6. /signin-microsoft │ │ │◄──────────────────│ │ │ │ + auth code │ │ │ │ 7. Exchange code │ │ │ │ using secret │ │ │ │ 8. Find/create │ │ │ │ user, mint a │ │ │ │ one-time code │ │ │ 9. 302 to loopback │ │ │ │ ?code=…&state=… │ │ │ 10. Capture code │◄───────────────────│ │ │◄───────────────────│ │ │ │ POST /auth/token ──────────────────────►│ │ │ code + verifier │ │ │ │◄──── JWT + refresh token ───────────────│ │ │ 11. DPAPI encrypt + │ │ │ │ persist │ │ │ │ 12. Start sync │ │ │ └────────────────────┴────────────────────┴───────────────────┘Step-by-step
Section titled “Step-by-step”- The desktop app finds a free TCP port, starts an
HttpListeneronhttp://localhost:{port}/, and generates a PKCE verifier plus its S256 challenge and astatenonce. - It opens the system browser to
/auth/login?provider=microsoft&redirect_uri=http://localhost:{port}/&code_challenge=<S256>&code_challenge_method=S256&state=<nonce>. - The server’s
/auth/loginendpoint requires the S256 challenge from a loopback caller — see the “Older desktop clients” note below. It also rejects a provider this deployment never configured. - The server returns
Results.Challenge(), which 302-redirects to Microsoft’s authorization endpoint. - The user signs in at Microsoft.
- Microsoft redirects back to the server’s
/signin-microsoftwith an authorization code. - ASP.NET Core middleware exchanges the code for tokens using the
ClientSecret. - The server extracts user claims and finds or creates the
PiaUser. An email already registered under a different sign-in method is refused here withemail_exists. - The server redirects the browser to the desktop’s loopback URI with a one-time login code and the echoed
stateas query parameters — not tokens. - The desktop’s loopback listener captures the code and
POSTs it with the PKCE verifier to/auth/token. The server returns the JWT access token and refresh token in the response body, only if the verifier matches. The code is single-use and short-lived. - The desktop encrypts the tokens with Windows DPAPI and persists them to
settings.json. - First-sync migration runs and background sync starts.
Key files
Section titled “Key files”| File | Role |
|---|---|
src/Pia.Server/Auth/OAuthConfiguration.cs |
Registers Microsoft OAuth middleware |
src/Pia.Server/Controllers/Auth/AuthController.cs |
/auth/login, /auth/callback, /auth/token, /auth/refresh |
src/Pia.Server/Auth/OAuthCallbackService.cs |
Callback logic: claims, user lookup, login code vs. admin cookie |
src/Pia.Server/Auth/LoginCodeExchangeService.cs |
One-time login code redemption against the PKCE verifier |
src/Pia.Server/Auth/JwtService.cs |
JWT generation, refresh token hashing |
src/Pia.Server/appsettings.json |
OAuth:Microsoft configuration |
The client side — loopback listener, PKCE, token storage and the login buttons — lives in src/Pia.Wpf/Services/AuthService.cs in the Pia.Wpf repo.