DEV Community

Cover image for Implementing OWIN Authentication and roles for AppService in ASP.NET Framework
David💻
David💻 Subscriber

Posted on

Implementing OWIN Authentication and roles for AppService in ASP.NET Framework

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.

image1

Configuring the Default Domain

After our service has been created, we will navigate to the dashboard and copy the "Default Domain" value.

image2

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"

image3

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)

image4

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

image5

image6

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;

image8

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)

image8

<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>
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
                    }
                }
            });
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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"
        }
    ],
Enter fullscreen mode Exit fullscreen mode

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

RolesAzure

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 });
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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

image10

Testing the Application

Finally, we verify the access:

User authenticated but without a role

Role1

User authenticated with an assigned role

Role2

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)