Yesterday we showed you how easy it is to add a simple authentication mechanism to your web application using OKTA but sometimes that is not enough specially when you want to have Role Based Authentication, you cannot just simply use the Authorize attribute and add Roles on it, that will just go on an endless loop of failed request. But like our post yesterday it is easily achievable but we will change the Owin AppBuilder to use OpenId Connect Authentication rather than Okta MVC.
Lets start.
First lets configure your API to handle this request.
Go to Security -> API -> Authorization Servers and choose the default one
Click default -> Claims ->then Add Claim
What we will be doing next is to add a Claim to include all groups on any scope
So in the name type “groups”,
Select ID Token then choose Always
Set Value type to Groups
Then on Filter choose Regex and in the text box type .*
check the Disable claim checkbox and choose Any scope radio button.
Then change your Startup .cs to this
using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using IdentityModel.Client; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.OpenIdConnect; using Owin; [assembly: OwinStartup(typeof(OKTA_Authentication.Startup))] namespace OKTA_Authentication { public class Startup { // These values are stored in Web.config. Make sure you update them! private readonly string clientId = ConfigurationManager.AppSettings["okta:ClientId"]; private readonly string redirectUri = ConfigurationManager.AppSettings["okta:RedirectUri"]; private readonly string authority = ConfigurationManager.AppSettings["okta:OrgUri"]; private readonly string clientSecret = ConfigurationManager.AppSettings["okta:ClientSecret"]; private readonly string postLogoutRedirectUri = ConfigurationManager.AppSettings["okta:PostLogoutRedirectUri"]; public void Configuration(IAppBuilder app) { app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); app.UseCookieAuthentication(new CookieAuthenticationOptions()); app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions { ClientId = clientId, ClientSecret = clientSecret, Authority = authority, RedirectUri = redirectUri, ResponseType = OpenIdConnectResponseType.CodeIdToken, Scope = OpenIdConnectScope.OpenIdProfile, PostLogoutRedirectUri = postLogoutRedirectUri, TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name" }, Notifications = new OpenIdConnectAuthenticationNotifications { AuthorizationCodeReceived = async n => { // Exchange code for access and ID tokens var tokenClient = new TokenClient(authority + "/v1/token", clientId, clientSecret); var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, redirectUri); if (tokenResponse.IsError) { throw new Exception(tokenResponse.Error); } var userInfoClient = new UserInfoClient(authority + "/v1/userinfo"); var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken); var claims = new List<Claim>(); claims.AddRange(userInfoResponse.Claims); claims.Add(new Claim("id_token", tokenResponse.IdentityToken)); claims.Add(new Claim("access_token", tokenResponse.AccessToken)); if (!string.IsNullOrEmpty(tokenResponse.RefreshToken)) { claims.Add(new Claim("refresh_token", tokenResponse.RefreshToken)); } n.AuthenticationTicket.Identity.AddClaims(claims); //Get all User Application Groups foreach (var group in userInfoResponse.Claims.Where(x => x.Type == "groups")) { n.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Role, group.Value)); } return; }, RedirectToIdentityProvider = n => { // If signing out, add the id_token_hint if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout) { var idTokenClaim = n.OwinContext.Authentication.User.FindFirst("id_token"); if (idTokenClaim != null) { n.ProtocolMessage.IdTokenHint = idTokenClaim.Value; } } return Task.CompletedTask; } }, }); } } }
Now that will work instantaneously, just use it like such
[Authorize(Roles = "Users")] public ActionResult Users() => View(); [Authorize(Roles = "Admin")] public ActionResult Admin() => View();
But make sure you have those group name in your application, this Group Names are the one you identified on the Roles on your Authorize Attribute.
Like I said the authorization should work, but there is one issue. If a user is logged in but does not belong to the right group, the default AuthorizeAttribute will attempt to redirect the user to the login screen for authentication, then the login screen will see that the user is already authenticated hence redirecting him back to the redirectUri with the token, then it will rebuild the ClaimsPrincipal and try to redirect the user back to the URL they originally requested then you are trapped in an endless loop, so take note.
To overcome that we need to Create a Custom ASP.NET MVC Attribute to avoid this problem. We just need to override the default behavior when the user is unauthorized, so lets inherit the AuthorizeAttribute by creating a class called OktaAuthorize and override the HandleUnauthorizedRequest method like such.
using System.Web.Mvc; using System.Web.Routing; namespace OKTA_Authentication.Helpers { public class OktaAuthorize : AuthorizeAttribute { protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (!filterContext.HttpContext.User.Identity.IsAuthenticated) { base.HandleUnauthorizedRequest(filterContext); } else { filterContext.Result = new RedirectToRouteResult( new RouteValueDictionary( new { controller = "Error", action = "AccessDenied" } ) ); } } } }
Now lets create an Error Controller to handle the view you will be presenting, like such
using System.Web.Mvc; namespace OKTA_Authentication.Controllers { public class ErrorController : Controller { // GET: Error public ActionResult AccessDenied() { return View(); } } }
then for the view it will be simple as this
@{ ViewBag.Title = "Access Denied"; Layout = "~/Views/Shared/_Layout.cshtml"; } <h2>Access Denied</h2>
Now you can replace your AuthorizeAttributes with OktaAuthorize and add roles on it like such
using System.Web.Mvc; using OKTA_Authentication.Helpers; namespace OKTA_Authentication.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } [OktaAuthorize(Roles = "Admin")] public ActionResult AdminsOnly() { ViewBag.Message = "Your application description page."; return View(); } [OktaAuthorize(Roles = "Super Admin")] public ActionResult SuperAdminsOnly() { ViewBag.Message = "Your contact page."; return View(); } } }
Now I think we have one more thing left to complete the series. What we missed is how to Use and Present the User Attributes in Okta such as ID, email, First Name, Last Name, Employee Number, etc. So next week we will show you how it is easily done. Till next time, follow this space.