Authentication & SSO (Microsoft Entra ID)

The platform uses Microsoft Entra ID (formerly Azure AD) as its single identity provider. All APIs and the web UI authenticate via OAuth 2.0 / OpenID Connect tokens, and it plugs straight into Microsoft 365 / Teams. This page covers tenant setup, app registration, token acquisition, the On-Behalf-Of flow, token refresh, and sample ID/access-token claims.


1. Setting up Microsoft Entra ID

The Entra ID admin needs to prepare three things:

  • Tenant ID (e.g. contoso.onmicrosoft.com)
  • App registrations (one for the Web app, and a separate mobile/desktop registration if you use Teams SSO)
  • Client ID / client secret (Web app only)
■ Redirect URI

Register the following as the Web-app redirect URI:

https://your-app.example.com/signin-oidc

2. Authorization code flow (standard Web app)

Browser sign-in uses OAuth 2.0 Authorization Code Flow. The ASP.NET Core middleware (Microsoft.Identity.Web) handles this for you — no manual implementation required on the application side.

■ Flow overview
  • 1. User hits /signin
  • 2. Redirected to the Entra ID sign-in page
  • 3. Authorization code returns to the redirect URI
  • 4. Server exchanges the code for access + ID + refresh tokens
  • 5. Tokens are stored in the session cookie
// Program.cs / Startup.cs
builder.Services
    .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

3. Acquiring and using tokens

Use the server-held access token when calling APIs. Expired tokens are refreshed automatically via the refresh token.

■ C# (Microsoft.Identity.Web) — calling a downstream API
public class ProjectsController : Controller
{
    private readonly ITokenAcquisition _tokens;

    public async Task<IActionResult> Index()
    {
        var token = await _tokens.GetAccessTokenForUserAsync(
            new[] { "api://{ClientId}/access_as_user" });

        var client = new HttpClient();
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        var res = await client.GetAsync("https://api.yourdomain.com/api/projects");
        return Ok(await res.Content.ReadAsStringAsync());
    }
}
■ curl — hitting the API with the acquired token
curl https://api.yourdomain.com/api/projects \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1Qi..."

4. On-Behalf-Of flow (Teams SSO → API)

To call the platform APIs from a Microsoft Teams tab app while preserving the user context, use the On-Behalf-Of (OBO) flow.

  • 1. The Teams client obtains an SSO token via getAuthToken()
  • 2. The token is scoped to the Teams app registration's client ID
  • 3. The server exchanges that token via OBO for an access token targeted at the platform API
  • 4. The server calls the API with that access token
■ C# — OBO token exchange
// Receive the Teams SSO token in Authorization header,
// exchange for a platform-API token via OBO.
var teamsToken = Request.Headers["Authorization"]
    .ToString().Replace("Bearer ", "");

var platformToken = await _tokens.GetAccessTokenForUserAsync(
    scopes: new[] { "api://{PlatformApiClientId}/access_as_user" },
    userFlow: null,
    authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme,
    user: null,
    tokenAcquisitionOptions: new TokenAcquisitionOptions
    {
        LongRunningWebApiSessionKey = TokenAcquisitionOptions.LongRunningWebApiSessionKeyAuto
    });

5. Audience (aud) and scopes

Access tokens targeting the API must carry the correct audience for that resource.

The platform's API audience is api://{ClientId} (where ClientId is the Web API app registration's value).

■ Common scopes
  • access_as_user — access_as_user — delegated user access to all API endpoints
  • User.Read — User.Read — minimum scope for Microsoft Graph (profile / email)

6. Token refresh

Access tokens expire after ~1 hour by default. With Microsoft.Identity.Web they're refreshed automatically using the refresh token. If you handle refresh tokens manually, follow these rules:

  • Keep refresh tokens server-side only — never return them to the browser
  • Don't use the same refresh_token concurrently (rotation detection invalidates them)
  • On invalid_grant, always redirect to the sign-in page

7. Sample ID and access token claims

The most useful claims to decode in your application. Samples below are real shapes with some values masked.

■ ID token — sample claims
{
  "aud": "{ClientId}",
  "iss": "https://login.microsoftonline.com/{TenantId}/v2.0",
  "iat": 1737123456,
  "exp": 1737127056,
  "name": "山田 太郎",
  "preferred_username": "yamada@contoso.onmicrosoft.com",
  "oid": "00000000-0000-0000-0000-000000000000",
  "tid": "{TenantId}",
  "ver": "2.0"
}
■ Access token — sample claims
{
  "aud": "api://{ClientId}",
  "iss": "https://sts.windows.net/{TenantId}/",
  "iat": 1737123456,
  "exp": 1737127056,
  "scp": "access_as_user",
  "oid": "00000000-0000-0000-0000-000000000000",
  "tid": "{TenantId}",
  "ver": "2.0"
}

8. Common errors and fixes

  • aud mismatch → make sure you explicitly request the API app registration's scope (e.g. access_as_user)
  • AADSTS65001 (consent_required) → tenant admin consent isn't granted yet. Have the admin grant consent once
  • Signature validation fails → check the public-key cache (Microsoft.Identity.Web usually refreshes it automatically)
  • Repeated 401 Unauthorized → the access token is probably expired; refresh and retry

Next: REST API docs →