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:
- Microsoft.Owin.Security.Cookies
- Microsoft.Owin.Security.OpenIdConnect
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Graph
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:
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:
<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:
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.
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.
// 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:
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:
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:
// 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:
@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:
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.
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:
<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:
<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
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:
<%@ 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>