Skip to content

Authentication

Setup

Local Development URL

In order for authentication to work in local development, you must update the project URL to https://localhost:7041. This is because our app registration in Azure is configured to accept this localhost address.

  • In the Solution Explorer, right click on your project, and select Settings at the bottom of the context menu
  • In your project settings, navigate to the Web tab
  • In the Web tab, under Servers, update the project URL field to https://localhost:7041, and click Create Virtual Directory

Once these steps are completed, the app will open to https://localhost:7041 in the browser.

Dependencies

Install the following dependencies from NuGet:

After these are installed, make sure to update any co-dependencies installed alongside them as well.

Code

Create a new folder called Utils in your app, and then a new Class file in it called Authentication.cs with the following code:

Utils/Authentication.cs
13 collapsed lines
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
namespace APP_NAME.App_Code
{
/// <summary>
/// Provides helper functions for authentication with Microsoft Entra ID.
/// </summary>
public class Authentication
{
/// <summary>
/// OAuth scopes required for authentication.
/// </summary>
private static readonly string[] _scopes = new[] { "openid", "profile", "email", "offline_access" };
/// <summary>
/// Default size for the currently authenticated user's profile photo.
/// </summary>
private const string _photoSize = "48x48";
/// <summary>
/// Configures the OWIN middleware pipeline with Entra ID authentication.
/// </summary>
/// <param name="app">The OWIN app builder.</param>
/// <param name="clientId">The Entra ID application client ID.</param>
/// <param name="clientSecret">The Entra ID application client secret.</param>
/// <param name="tenantId">The Entra ID tenant ID.</param>
/// <param name="redirectUri">The URI to redirect to after authentication.</param>
public static void UseAuth(IAppBuilder app, string clientId, string clientSecret, string tenantId, string redirectUri)
{
var authority = $"https://login.microsoftonline.com/{tenantId}/v2.0";
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
167 collapsed lines
ClientId = clientId,
ClientSecret = clientSecret,
Authority = authority,
RedirectUri = redirectUri,
Scope = String.Join(" ", _scopes),
ResponseType = "token id_token",
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = (ctx) =>
{
var accessToken = ctx.ProtocolMessage.AccessToken;
if (!string.IsNullOrEmpty(accessToken))
ctx.AuthenticationTicket.Identity.AddClaim(new Claim("access_token", accessToken));
return Task.CompletedTask;
},
}
});
}
/// <summary>
/// Retrieves the currently authenticated user's information from Microsoft Graph.
/// </summary>
/// <returns>A User object containing the authenticated user's information.</returns>
/// <exception cref="UnauthorizedAccessException">Thrown when the user is not authenticated.</exception>
/// <exception cref="NullReferenceException">Thrown when user data cannot be fetched.</exception>
public static async Task<User> GetUserAsync()
{
var graph = GraphService.UseGraph();
var authResult = await GraphService.GetAuthenticationResultAsync();
if (authResult == null)
throw new UnauthorizedAccessException("User is not authenticated.");
var user = await graph.Me.GetAsync();
if (user == null)
throw new NullReferenceException("Failed to fetch user data.");
var profilePicture = await GetUserProfilePictureAsync(graph);
return new User
{
Id = user.Id,
ADName = user.UserPrincipalName.ToLower().Split('@')[0],
FullName = user.DisplayName,
JobTitle = user.JobTitle,
ProfilePicture = profilePicture,
};
}
/// <summary>
/// Retrieves the currently authenticated user's profile picture from Microsoft Graph.
/// </summary>
/// <param name="graph">The GraphServiceClient instance to use.</param>
/// <returns>A base64-encoded data URI string containing the profile picture.</returns>
/// <exception cref="NullReferenceException">Thrown when profile picture cannot be retrieved.</exception>
private static async Task<string> GetUserProfilePictureAsync(GraphServiceClient graph)
{
var photoStream = await graph.Me.Photos[_photoSize].Content.GetAsync();
if (photoStream == null)
throw new NullReferenceException("Failed to fetch user profile picture.");
using (var memoryStream = new MemoryStream())
{
await photoStream.CopyToAsync(memoryStream);
return $"data:image/png;base64,{Convert.ToBase64String(memoryStream.ToArray())}";
}
}
/// <summary>
/// Represents a user authenticated with Entra ID.
/// </summary>
public class User
{
/// <summary>
/// Gets or sets the unique identifier for the user.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets the Active Directory username (without domain).
/// </summary>
public string ADName { get; set; }
/// <summary>
/// Gets or sets the user's full name.
/// </summary>
public string FullName { get; set; }
/// <summary>
/// Gets or sets the user's job title.
/// </summary>
public string JobTitle { get; set; }
/// <summary>
/// Gets or sets the user's profile picture as a base64-encoded data URI.
/// </summary>
public string ProfilePicture { get; set; }
}
}
/// <summary>
/// Provides services for interacting with Microsoft Graph API.
/// </summary>
public class GraphService
{
/// <summary>
/// The singleton instance of GraphServiceClient.
/// </summary>
private static readonly GraphServiceClient _graphClient;
/// <summary>
/// Static constructor to initialize the GraphServiceClient with appropriate credentials.
/// </summary>
static GraphService()
{
var credentials = new BaseBearerTokenAuthenticationProvider(new TokenProvider());
_graphClient = new GraphServiceClient(credentials);
}
/// <summary>
/// Gets the configured GraphServiceClient instance.
/// </summary>
/// <returns>The configured GraphServiceClient.</returns>
public static GraphServiceClient UseGraph() => _graphClient;
/// <summary>
/// Gets the authentication result for the current HTTP context.
/// </summary>
/// <returns>The authentication result, or null if not authenticated.</returns>
public static async Task<AuthenticateResult> GetAuthenticationResultAsync() =>
await HttpContext.Current.GetOwinContext().Authentication.AuthenticateAsync("Cookies");
}
/// <summary>
/// Provides access tokens for Microsoft Graph API requests.
/// </summary>
public class TokenProvider : IAccessTokenProvider
{
/// <summary>
/// Gets the validator for allowed hosts.
/// </summary>
public AllowedHostsValidator AllowedHostsValidator { get; }
/// <summary>
/// Gets the authorization token for the specified URI.
/// </summary>
/// <param name="uri">The URI requiring authorization.</param>
/// <param name="additionalAuthenticationContext">Additional authentication context, if any.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The access token for the request.</returns>
/// <exception cref="UnauthorizedAccessException">Thrown when the user is not authenticated or the token is not found.</exception>
public async Task<string> GetAuthorizationTokenAsync(Uri uri, Dictionary<string, object> additionalAuthenticationContext = default, CancellationToken cancellationToken = default)
{
var res = await GraphService.GetAuthenticationResultAsync();
if (res == null || !res.Identity.IsAuthenticated)
throw new UnauthorizedAccessException("User is not authenticated.");
var accessToken = res.Identity.FindFirst("access_token")?.Value;
if (string.IsNullOrEmpty(accessToken))
throw new UnauthorizedAccessException("Access token not found");
return accessToken;
}
}
}

Secrets

Create a new configuration file called secrets.config in your app with the following code:

secrets.config
<appSettings>
<!-- Azure Tenant -->
<add key="Azure:TenantId" value="TENANT_ID" />
<!-- Authentication -->
<add key="Auth:ClientId" value="CLIENT_ID" />
<add key="Auth:ClientSecret" value="CLIENT_SECRET" />
<add key="Auth:RedirectUri_Dev" value="https://localhost:7041/signin-oidc" />
<add key="Auth:RedirectUri" value="https://PRODUCTION_URL/signin-oidc" />
<!-- Dev Mode Flag -->
<add key="IsDev" value="true"/>
</appSettings>

Middleware

Create a new OWIN startup class called Startup.cs in your app with the following code:

Startup.cs
4 collapsed lines
using Microsoft.Owin;
using Owin;
using System.Configuration;
using APP_NAME.App_Code;
/// <summary>
/// Specifies the startup type to be used by the OWIN middleware.
/// </summary>
[assembly: OwinStartup(typeof(APP_NAME.Startup))]
namespace APP_NAME
{
/// <summary>
/// Configures the OWIN middleware pipeline for the application.
/// </summary>
public class Startup
{
/// <summary>
/// The Entra ID application client ID from configuration.
/// </summary>
private static readonly string _clientId = ConfigurationManager.AppSettings["Auth:ClientId"];
/// <summary>
/// The Entra ID application client secret from configuration.
/// </summary>
private static readonly string _clientSecret = ConfigurationManager.AppSettings["Auth:ClientSecret"];
/// <summary>
/// The Azure tenant ID from configuration.
/// </summary>
private static readonly string _tenantId = ConfigurationManager.AppSettings["Azure:TenantId"];
/// <summary>
/// The redirect URI based on environment configuration.
/// Uses production URI by default or if IsDev parsing fails.
/// Uses development URI only when IsDev is explicitly set to true.
/// </summary>
private static readonly string _redirectUri = !bool.TryParse(ConfigurationManager.AppSettings["IsDev"], out bool isDev) || !isDev
? ConfigurationManager.AppSettings["Auth:RedirectUri"]
: ConfigurationManager.AppSettings["Auth:RedirectUri_Dev"];
/// <summary>
/// Configures the application's OWIN middleware pipeline.
/// </summary>
/// <param name="app">The OWIN application builder instance.</param>
public void Configuration(IAppBuilder app)
{
Authentication.UseAuth(app, _clientId, _clientSecret, _tenantId, _redirectUri);
}
}
}

Usage — MVC

Protecting Routes

Specific Pages

To require authentication on specific pages, apply the [Authorize] attribute to the page’s render action.

Controllers/PagesController.cs
public class PagesController : Controller
{
// These pages require authentication
[Authorize]
public ActionResult PageOne() => View();
[Authorize]
public ActionResult PageTwo() => View();
// This page is publicly accessible
public ActionResult PageThree() => View();
}

Specific Controller

To require authentication on all pages within a specific controller, apply the [Authorize] attribute to the entire controller.

Controllers/PagesController.cs
// Every page rendered by this controller requires authentication
[Authorize]
public class PagesController : Controller
{
public ActionResult PageOne() => View();
public ActionResult PageTwo() => View();
public ActionResult PageThree() => View();
}

Entire App

To require authentication throughout the entire app, create a Class file called FilterConfig.cs in the App_Start folder with the following code:

App_Start/FilterConfig.cs
using System.Web.Mvc;
namespace APP_NAME
{
/// <summary>
/// Configures global filters for the application.
/// </summary>
public class FilterConfig
{
/// <summary>
/// Registers the AuthorizeAttribute globally, requiring authentication
/// for all controllers and actions.
///
/// Use [AllowAnonymous] to exempt specific pages from this requirement.
/// </summary>
/// <param name="filters">The global filter collection.</param>
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new AuthorizeAttribute());
}
}
}

Then, apply your filter under Application_Start() in your app’s global.asax file:

global.asax
using System.Web.Mvc;
using System.Web.Routing;
namespace APP_NAME
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
}
}
}

Getting User Data

To get data of the currently authenticated user, you can use the getUserAsync() method from Authentication.cs in your controllers.

Example

Define page model:

Models/IndexPageModel.cs
// Other imports...
using APP_NAME.Utils;
namespace APP_NAME.Models
{
public class IndexPageModel
{
// Other props...
public Authentication.User UserData { get; set; }
}
}

Create page view, assign page model, and display user data:

Views/Pages/Index.cshtml
@model APP_NAME.Models.IndexPageModel
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index Page</title>
</head>
<body>
<p>Username: @Model.UserData.ADName</p>
<p>Name: @Model.UserData.FullName</p>
<p>Job: @Model.UserData.JobTitle</p>
<img src="@Model.UserData.ProfilePicture" alt="@Model.UserData.FullName profile picture" width="48" height="48" />
</body>
</html>

Create a render action for the view in its controller, fetch data, and pass the data to view via the model:

Controllers/PagesController.cs
4 collapsed lines
using System.Threading.Tasks;
using System.Web.Mvc;
using APP_NAME.Models;
using APP_NAME.Utils;
namespace APP_NAME.Controllers
{
public class PagesController : Controller
{
public async Task<ActionResult> Index()
{
var userData = await Authentication.GetUserAsync();
var model = new IndexPageModel
{
// Existing props...
UserData = userData,
};
return View(model);
}
}
}

Usage — Web Forms

Protecting Routes

Specific Pages

To require authentication on specific pages, apply the [Authorize] attribute to the page’s code-behind class.

YourPage.aspx.cs
namespace APP_NAME
{
// This page requires authentication
[Authorize]
public partial class YourPage : System.Web.UI.Page
{
// Page code
}
}

Specific Folders

To require authentication on all pages within a specific folder, create a new configuration file also called web.config inside the folder with the following code:

Folder/web.config
<configuration>
<system.web>
<!-- Existing configuration -->
<authorization>
<deny users="?" />
</authorization>
</system.web>
</configuration>

Entire App

To require authentication throughout the entire app, update your app’s web.config file with the following code:

web.config
<configuration>
<system.web>
<!-- Existing configuration -->
<authorization>
<deny users="?" />
</authorization>
</system.web>
</configuration>

Getting User Data

To get data of the currently authenticated user, you can use the getUserAsync() method from Authentication.cs in a page’s code-behind file.

Example

Fetch and expose data to frontend

Pages/Default.aspx.cs
using System;
using APP_NAME.Utils;
namespace APP_NAME
{
public partial class Default : System.Web.UI.Page
{
public Authentication.User UserData { get; set; }
protected async void Page_Load(object sender, EventArgs e)
{
var userData = await Authentication.GetUserAsync();
UserData = userData;
}
}
}

Enable async and display user data:

Pages/Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="APP_NAME.Default" Async="true" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Index Page</title>
</head>
<body>
<form runat="server">
<div>
<p>Username: <%= UserData.ADName %></p>
<p>Name: <%= UserData.FullName %></p>
<p>Job: <%= UserData.JobTitle %></p>
<img
src="<%= UserData.ProfilePicture %>"
alt="<%= UserData.FullName %> profile picture"
width="48"
height="48"
/>
</div>
</form>
</body>
</html>