This article purpose is to describe how to secure our ASP.NET Framework application following a layer structure using OWIN as a middleware and Microsoft Entra ID. This comprehensive guide walks you through the creation of an App that will be published to Azure App Service.
Requirements
- Visual Studio 2022
- Azure Subscription
Walkthrough
Creating the App Service
Within our Azure subscription, we will create an App Service of type Web App.
In the template, we will fill in basic data:
- Select our Azure subscription
- Create a resource group if one doesn't already exist
- Give our application a name
- We will publish the code directly from Visual Studio 2022
- Our Runtime will be ASP.NET V4.8
- Choose the Region and plan that best suits our needs
- For this example, we will select a less crowded region and proceed with the Free plan
Proceed with the "Review + Create" option.
Configuring the Default Domain
After our service has been created, we will navigate to the dashboard and copy the "Default Domain" value.
Registering the Application in Microsoft Entra ID
Now we will navigate within our Azure Portal to Microsoft Entra ID > Manage > App Registrations.
Within this window, we will register the application by clicking the "New Registration" option.
In this window, we will:
- Register the name of our application
- Define which Tenants can access the application
- Register the URI by copying the App Service application URL as "Web"
Configuring Authentication
After having our application registered, in the same dashboard of our registered application, we navigate to Manage > Authentication.
In this window, we will activate the following options:
- ID tokens (used for implicit and hybrid flows)
- Access tokens (used for implicit and hybrid flows)
Creating the Application in Visual Studio 2022
We will create a new project with the following template:
- ASP.NET Web Application (.NET Framework)
- Select runtime: .NET Framework 4.8
In the next window:
- Select type: Empty & Web Api
- Authentication: None
Installing Required Dependencies
Now we will proceed to install the necessary dependencies to run the project. Within the project:
Right-click > Manage NuGet Packages for Solution
Search for and install the following packages:
- System.Linq;
- System.Security.Claims;
- Microsoft.IdentityModel.Protocols.OpenIdConnect;
- Microsoft.IdentityModel.Tokens;
- Microsoft.Owin.Host.SystemWeb;
- Microsoft.Owin.Security;
- Microsoft.Owin.Security.Cookies;
- Microsoft.Owin.Security.OpenIdConnect;
- Owin;
- System.Configuration;
- System.IdentityModel.Tokens.Jwt;
- Microsoft.Owin;
Modifying Web.Config
After installing our dependencies, we start by modifying the Web.Config file.
Within appSettings, we proceed to modify the following values.
From Microsoft Entra ID, in App registrations, we look for our application and copy the following values:
- ClientID
- TenantID
- RedirectUri (that we configured in App Service)
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
<add key="ida:ClientId" value="<your-client-id>" />
<add key="ida:TenantId" value="<your-tenant-id>" />
<add key="ida:Authority" value="https://login.microsoftonline.com/<your-tenant-id>/v2.0" />
<add key="ida:RedirectUri" value="https://<your-application>.azurewebsites.net/" />
</appSettings>
<system.web>
<authentication mode="None" />
<compilation debug="true" targetFramework="4.8" />
<httpRuntime targetFramework="4.8" />
<customErrors mode="Off"/>
</system.web>
<system.webServer>
<modules>
<remove name="FormsAuthentication" />
</modules>
</system.webServer>
Creating Startup.cs
Now we will proceed with the creation of a file in the root called Startup.cs
which is the entry point of an ASP.NET application that uses OWIN; it’s where we define how our app boots up and handles requests.
using System.Web.Http;
using Owin;
[assembly: Microsoft.Owin.OwinStartup(typeof(TestOwinRoles.Startup))]
namespace OwinAuthApp
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
// auth middleware
ConfigureAuth(app);
// web API
var config = new HttpConfiguration();
// attribute routing (required for [Route]/[RoutePrefix])
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
}
Creating Startup.Auth.cs
Now we will proceed with the creation of a file in the folder App_start
called Startup.Auth.cs
which is where our app’s sign-in is set up. It reads our Azure Entra ID settings, turns on cookie authentication so users stay logged in, and enables OpenID Connect so the Microsoft login page is used for authentication. It also fine tunes claims: it keeps the "roles" claim intact, maps those roles so User.IsInRole(...)
works.
using System.Linq;
using System.Security.Claims;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Host.SystemWeb;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System.Configuration;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.Owin;
namespace OwinAuthApp
{
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
var clientId = ConfigurationManager.AppSettings["ida:ClientId"];
var authority = ConfigurationManager.AppSettings["ida:Authority"];
var redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
CookieName = "OwinAuthApp",
CookieSecure = CookieSecureOption.Always,
CookieSameSite = SameSiteMode.None,
ExpireTimeSpan = System.TimeSpan.FromHours(1),
SlidingExpiration = true,
CookieManager = new SystemWebCookieManager()
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.IdToken,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
NameClaimType = "name",
RoleClaimType = "roles"
},
CookieManager = new SystemWebCookieManager(),
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = context =>
{
var id = (ClaimsIdentity)context.AuthenticationTicket.Identity;
var v2Roles = id.FindAll("roles").Select(c => c.Value).ToList();
foreach (var r in v2Roles)
id.AddClaim(new Claim(ClaimTypes.Role, r));
// this through the route /token exposes the JWT token for debugging purposes
var raw = context.ProtocolMessage.IdToken;
if (!string.IsNullOrEmpty(raw))
id.AddClaim(new Claim("raw_id_token", raw));
return System.Threading.Tasks.Task.FromResult(0);
},
RedirectToIdentityProvider = context =>
{
// force re-login only when you explicitly ask: ?relogin=1
if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
if ("1".Equals(context.OwinContext.Request.Query.Get("relogin")))
{
context.ProtocolMessage.Prompt = "login";
context.ProtocolMessage.MaxAge = "0";
}
}
return System.Threading.Tasks.Task.FromResult(0);
},
AuthenticationFailed = context =>
{
System.Diagnostics.Trace.TraceError("OIDC Auth Failed: " + context.Exception);
context.HandleResponse();
var msg = System.Uri.EscapeDataString(context.Exception.Message);
context.Response.Redirect("/error.html?message=" + msg);
return System.Threading.Tasks.Task.FromResult(0);
}
}
});
}
}
}
Now we are going to create some roles to test our app. For this we go into Azure portal > App registration > Our App > Manifest > AppRoles
Inside our manifest which should be a json object create the following roles:
"appRoles": [
{
"allowedMemberTypes": [
"User"
],
"description": "Regular users",
"displayName": "User",
"id": "00000000-0000-0000-0000-000000000002",
"isEnabled": true,
"origin": "Application",
"value": "User"
},
{
"allowedMemberTypes": [
"User"
],
"description": "Administrators can do admin things",
"displayName": "Admin",
"id": "00000000-0000-0000-0000-000000000001",
"isEnabled": true,
"origin": "Application",
"value": "Admin"
}
],
Now still at Azure portal > Enterprise Application > Our App > Users and groups > Add user/group
Here we will assign our tenant users into the previously created roles
Creating Controllers
Now we proceed to create a controller which will expose our API routes. Create the file SecureController.cs
inside the folder Controllers.
In this controller we will expose 4 API's which we should access like: https://.azurewebsites.net/api/secure/
- info: Expose the current user authenticated and current role
- claims: Expose all current claims with type-value
- token: Expose the JWT token for the session
- test: A simple endpoint to test if the application is up
using System;
using System.Linq;
using System.Security.Claims;
using System.Web.Http;
namespace OwinAuthApp.Controllers
{
[Authorize]
[RoutePrefix("api/secure")]
public class SecureController : ApiController
{
[HttpGet]
[Route("info")]
public IHttpActionResult GetInfo()
{
var id = User?.Identity as ClaimsIdentity;
var rolesV2 = id?.FindAll("roles").Select(c => c.Value).ToArray() ?? new string[0];
var rolesV1 = id?.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray() ?? new string[0];
var anyRoles = rolesV2.Concat(rolesV1).Distinct().ToArray();
var isAdmin = User.IsInRole("Admin") || anyRoles.Contains("Admin");
var isUser = User.IsInRole("User") || anyRoles.Contains("User");
var role = isAdmin ? "Admin" : isUser ? "User" : "Unassigned";
var message = isAdmin ? "Welcome, Admin!" :
isUser ? "Hello, User!" :
"You don’t have a role yet. Please contact an administrator.";
return Ok(new
{
isAuthenticated = id?.IsAuthenticated ?? false,
userName = id?.Name ?? "",
authType = User?.Identity?.AuthenticationType ?? "",
rolesV2,
rolesV1,
role,
message
});
}
[HttpGet]
[Route("claims")]
public IHttpActionResult Claims()
{
var ci = User?.Identity as ClaimsIdentity;
var claims = (ci?.Claims ?? Enumerable.Empty<Claim>())
.Select(c => new { c.Type, c.Value })
.ToList();
var rolesV2 = (ci?.FindAll("roles") ?? Enumerable.Empty<Claim>())
.Select(c => c.Value).ToArray();
var rolesV1 = (ci?.FindAll(ClaimTypes.Role) ?? Enumerable.Empty<Claim>())
.Select(c => c.Value).ToArray();
return Ok(new { rolesV2, rolesV1, claimsCount = claims.Count, claims });
}
[HttpGet]
[Route("token")]
public IHttpActionResult GetToken()
{
var ci = User as ClaimsPrincipal;
var token = ci?.FindFirst("raw_id_token")?.Value ?? "(none)";
return Ok(new { id_token = token });
}
[AllowAnonymous]
[HttpGet]
[Route("test")]
public IHttpActionResult Test()
{
return Ok(new { message = "Controller is working!", timestamp = DateTime.UtcNow });
}
}
}
Publishing to Azure
With our project ready, we will now proceed to publish it:
- Right-click on the project > Publish > Azure
- Authenticate with the user account that has access to our Azure subscription
- Select our resource group and app service
- Click Publish
Testing the Application
Finally, we verify the access:
User authenticated but without a role
User authenticated with an assigned role
Summary
The Startup.cs sets up the OWIN pipeline so every request passes through authentication and routing, while Startup.Auth.cs configures cookie authentication and OpenID Connect to handle Microsoft logins. We defined app roles in Entra, assigned users and guests to those roles, and mapped the "roles" claim from the ID token into the app so User.IsInRole() and [Authorize(Roles=...)]
work correctly. The end result is a secure API where users must log in with Microsoft accounts and see different responses depending on whether they are assigned as Admin or User
Top comments (0)