I'm working on a web application built using .NET Framework 4.8. The application has one side for the public and one for admins.
The application is registered on Azure, and I'm trying to use Azure AD groups to authenticate and authorize users.
I setup the Middleware for Azure authentication in a a Startup.Auth.cs partial class. The code in the partial class is shown at below.
To access the admin side, the user has to type /admin in the URL which will go to the admin controller.
I use a custom Authorize attribute in the Admin Controller with a specific Role. It's used before the initialization of the AdminController class.
The custom Authorize class code is shown below.
Locally, the application seems to be working fine and it allows people in the group specified in the authorize attribute and redirects ones without access to the Home like we want.
When we publish the code to Production, the application always takes the user back to home when they try to go to the admin side.
The redirectURI is setup to be "https://example.com/admin" and it is added to the application in Azure.
Using Authorize attribute in the admin controller allows anyone that is part of the Azure AD for the tenant.
Please let me know if I'm missing anything or what your thoughts are
Startup.Auth.cs
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ClientId"];
private static string aadInstance = ConfigurationManager.AppSettings["AADInstance"];
private static string tenantId = ConfigurationManager.AppSettings["TenantId"];
private static string redirectUri = ConfigurationManager.AppSettings["RedirectUri"];
private static string authority = aadInstance + tenantId + "/v2.0";
public void ConfigurationAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UserOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions()
{
ClientId = clientId;
Authority = authority;
RedirectUri = redirectUri;
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthenticationFailed = (context) => {
context.HandleResponse();
context.Response.Redirect("Home/index");
return Task.FromResult(0);
}
}
});
}
}
Custom authorize attribute AuthorizeAttribute.cs:
public class AuthorizeAD : AuthorizeAttribute
{
private bool noPermission = false;
protected override bool AuthorizationCore(HttpContextBase httpContext)
{
if(!httpContext.User.Identity.IsAuthenticated)
return false;
var roles = Roles.Trim().Split(',');
if(roles.ToList().Exists(role => httpContext.User.IsInRole(role)))
{
return true;
}
else
{
noPermission = true;
return false;
}
}
protected override void HandleUnAuthorizedRequest(AuthorizationContext filterContext)
{
if(noPermission)
filterContext.Result = new RedirectResult("Home/index");
else
base.HandleUnauthorizedRequest(filterContext);
}
}
Any help, feedback, or suggestions would be great. Thank you in advance!
• You can capture the return URL using the request information instead of using the ‘custom AuthorizeAttribute’ which will make your ‘returnURL’ or redirect URI available within ‘Request.QueryString[]’ dictionary. Also, you will need to add the below in your login view to make it actionable and the below it in your login form: -
#{
ViewBag.ReturnUrl = Request.QueryString["returnUrl"];
}
#using (Html.BeginForm("Login", "Account", new {returnUrl = ViewBag.ReturnUrl}, FormMethod.Post, new{#class="form-horizontal form-material", #onsubmit="return loading_event();", #id="loginForm"}))
Please find the below SO community thread for more reference and clarification on this: -
Custom Authorize attribute with redirect to original URL
Also, since you want to use the custom ‘AuthorizeAttribute’ class with a specific role for access in the ‘AdminController’ class, you can surely use them by leveraging the role-based authorization for the actions in the ‘AdminController’ class as follows: -
String constants in ‘Auth.cs’ : -
public static class RoleConstants
{
public const string Admin = "Admin";
public const string Moderator = "Moderator";
// more roles
}
‘AdminController’ class is as below after the above constant inclusion: -
[Authorize(Roles=RoleConstants.Admin+","+RoleConstants.Moderator)]
public class AdminController : Controller
{
// ...
}
Please find the below link for more information regarding the above: -
https://www.telerik.com/blogs/creating-custom-authorizeattribute-asp-net-core
Related
I am developing an intranet asp.net core web api application. The requirements for authentications are:
REQ1 - when user which is trying to access the website is not in Active Directory's special group (let's name it "commonUsers") it is simply not authorized
REQ2 - when user which is trying to access the website is in Active Directory's group "commonUsers" is is authorized and a web resource is returned
REQ3 - when user which is trying to access the website is in Active Directory's group "superUser", it need to be prompted for his domain password once again (because it tries to access some very restricted resources)
Now, what I have so far:
My service is hosted using http.sys server in order to support windows authentication.
I am using claims transformer middlewere in order to check the user's Active Directory group, let's say something like this:
public class ClaimsTransformer : IClaimsTransformation {
private readonly IAuthorizationService _authorizationService;
public ClaimsTransformer(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
_authorizationService.Authorize(principal as IHmiClaimsPrincipal);
return Task.FromResult(principal);
}}
I have specified a special policies also in my service configuration, for instance something like that:
services.AddAuthorization(options =>
{
options.AddPolicy("TestPolicy", policy =>
policy.RequireClaim(ClaimTypes.Role, "TestUser"));
options.AddPolicy("TestPolicy2", policy =>
policy.RequireClaim(ClaimTypes.Role, "SuperUser"));
});
I am using [Authorize] attribute with specific policy in order to restrict access to specific resources based on policies
Now the question is, how should I satisfy REQ3?
I think I would try to use MVC Filters : https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#authorization-filters
Filters run after all Middleware, but before the Action. This will allow you to control the redirect to credentials page just for specific actions or controllers. Whilst normally this is not the recommended method for authorization, I think it fits your requirements for a hybrid secondary authentication.
public class SuperUserFilter : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.Request.Cookies.TryGetValue("SuperUserCookie", out string cookieVal))
{
if (!IsValidCookie(cookieVal))
context.Result = LoginPage(context);
}
else
{
context.Result = LoginPage(context);
}
}
private bool IsValidCookie(string cookieVal)
{
//validate cookie value somehow
// crytpographic hash, store value in session, whatever
return true;
}
private ActionResult LoginPage(AuthorizationFilterContext context)
{
return new RedirectToActionResult("SuperUser", "Login",
new {redirectUrl = context.HttpContext.Request.GetEncodedUrl()});
}
}
Then you create a Login Controller
public class LoginController : Controller
{
[HttpGet]
public IActionResult SuperUser(string redirectUrl)
{
// return a page to enter credentials
// Include redirectUrl as field
}
[HttpPost]
public IActionResult SuperUser(LoginData loginData)
{
// Validate User & Password
Response.Cookies.Append("SuperUserCookie", "SomeValue");
return Redirect(loginData.RedirectUrl);
}
}
Then you can decorate specific actions (or controllers) as required:
public class MyController : Controller
{
[HttpGet]
[SuperUserFilter]
public IActionResult MySensitiveAction()
{
// Do something sensitive
}
}
I'm guessing you are try to implement two step authentication for some of your resource.
To do that you must use multiple authentication scheme and Authorize policies,
but it's difficult because windows authentication is not controllable. we need to use some trick to know this is your second login.
authentication
The Default Authenticaiton Scheme : Windows, it's the basic scheme for authenticate a windows user.
Second Cookies base Authentication scheme : SuperUserTwoStep. we need this to goto our custom login logic.
Authorize
the Authorize policies for specified scheme.
a login page for login to SuperUserTwoStep scheme.
//startup
services.AddAuthentication(HttpSysDefaults.AuthenticationScheme)
.AddCookie("SuperUserTwoStep",op=>op.LoginPath = "/account/superuser2steplogin");
services.AddAuthorization(op =>
{
op.AddPolicy("SuperUser", b => b.AddAuthenticationSchemes("SuperUserTwoStep")
.RequireAuthenticatedUser()
.RequireClaim(ClaimTypes.Role, "SuperUser"));
});
// login
public static IDictionary<string, string> States { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
[Route("/account/superuser2steplogin")]
public async Task<IActionResult> LoginTwoStepConfirm(string returnUrl, [FromServices]IAuthorizationService authorizationService,
[FromServices]IAuthorizationPolicyProvider policyProvider)
{
var winresult = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme);
if (winresult.Succeeded)
{
if (States.TryGetValue(winresult.Principal.Identity.Name, out _))
{
States.Remove(winresult.Principal.Identity.Name);
var principal = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity(winresult.Principal.Claims,"twostepcookie"));
await HttpContext.SignInAsync("SuperUserTwoStep", principal);
return Redirect(returnUrl);
}
else
{
States[winresult.Principal.Identity.Name] = "1";
return Challenge(IISDefaults.AuthenticationScheme);
}
}
else
{
return Challenge(IISDefaults.AuthenticationScheme);
}
}
[Authorize("SuperUser")]
public IActionResult YourSecurePage()
{
return Content("hello world");
}
the most difficult thing is to track that this is the second time to login, I try to use cookie , but it doen't work, so I crate a static IDitionary<string,string> to track ,maybe use distributed cache is better
I think in my opinion you should consider using: Policy-based authorization with Requirements, basically you have different authorization requirements that you want to treat them on and AND basis
REQ1 and REQ2 and REQ3
Here you have the link to the documentation: Requirements
But you need to understand that identity != permissions, the guys that introduce this concept of policies to Microsoft created a project named: PolicyServer and it is opensource: PolicyServer Git and they created a pattern there of how you should use your policies. Basically, you have external and internal users that are authenticated against your AD, all internal users should have permissions assigned to a role. And you only decorate your controller action with the permission rule you created for that policy
[Authorize("PerformSurgery")]
public async Task<IActionResult> PerformSurgery()
{
// omitted
}
To understand the code and how they evaluate a policy, I think you should see the video they have online on the website: Policy Server
Hope this helps
I am using signalR in asp.net mvc application,I want to authenticate cross
domain clients by token based authentication.I did not found complete solution for
it.
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
Provider = new QueryStringOAuthBearerProvider()
});
var hubConfiguration = new HubConfiguration
{
Resolver = GlobalHost.DependencyResolver,
};
map.RunSignalR(hubConfiguration);
});
public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
var value = context.Request.Query.Get("access_token");
if (!string.IsNullOrEmpty(value))
{
context.Token = value;
}
return Task.FromResult<object>(null);
}
}
public class impAuthHub : Hub
{
[Authorize]
public void SendMessage(string name, string message)
{
Clients.All.newMessage(name, message);
}
}
I dont know how i will get token to pass query string to my startup class?
You will be needed to use OAuth Bearer Token authentication with SignalR. and you need to use Microsoft’s OWIN Security and ASP.NET Identity libraries then include the WebAPI and Individual Accounts security options. This is a Full- Demo
Please find the code base for working sample git , which will help you.
I would like to create a layer for my Web API 2 Odata4 application to abstract the details of authentication so an end user can bolt on whichever Auth provider they choose. Right now the application is structured to authenticate the user in the UI using Microsoft B2C and redirect the user accordingly to the Angular2 page. All of the redirect logic to and from B2C is hard coded into the UI. At that point the token is extracted from the URL and is decoded as necessary to get username and ClientID information. When the UI makes a call to the API it passes that username and ClientID to the service which implements OAuth. The API matches the client ID with the acceptable list and serves data accordingly. Most of this process seems hard coded into both the UI and the API.
This is how the UI is grabbing the token:
sharedservice.id_token = location.hash.split('id_token=')[1];
authService.isLoggedIn = true;
fetch(sharedservice.JWKS_URL, {
method: 'GET'
}).then(res => res.json())
.then(json => {
this.getSigningKey(json.keys[0].kid, ()=>{})
})
This should only happen if the auth service has successfully authenticated the user. The token is then validated and the user information is setup.
The API gets the authentication service setup in the startup.auth.cs file under app_start and it looks like this:
// These values are pulled from web.config
public static string AadInstance = ConfigurationManager.AppSettings[" "];
public static string Tenant = ConfigurationManager.AppSettings[" "];
public static string ClientId1 = ConfigurationManager.AppSettings[" "];
public static string ClientId2 = ConfigurationManager.AppSettings[" "];
public static string ClientId3 = ConfigurationManager.AppSettings[" "];
public static string ClientId4 = ConfigurationManager.AppSettings[" "];
public static string SignUpSignInPolicy = ConfigurationManager.AppSettings[" "];
public static string DefaultPolicy = SignUpSignInPolicy;
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
// Web API routes
config.MapHttpAttributeRoutes();
ConfigureOAuth(app);
app.UseWebApi(config);
}
public void ConfigureOAuth(IAppBuilder app)
{
app.UseOAuthBearerAuthentication(CreateBearerOptionsFromPolicy(DefaultPolicy));
}
private OAuthBearerAuthenticationOptions CreateBearerOptionsFromPolicy(string policy)
{
var metadataEndpoint = string.Format(AadInstance, Tenant, policy);
TokenValidationParameters tvps = new TokenValidationParameters
{
// Accept only those tokens where the audience of the token is equal to the client ID of this app
ValidAudiences = new string[] { ClientId1, ClientId2, ClientId3, ClientId4 },
//ValidAudience = ClientId2,
AuthenticationType = Startup.DefaultPolicy
};
return new OAuthBearerAuthenticationOptions
{
// This SecurityTokenProvider fetches the Azure AD B2C metadata & signing keys from the OpenIDConnect metadata endpoint
AccessTokenFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(metadataEndpoint))
};
}
All of the B2C configuration seems to be hard coded. What is the most acceptable way to abstract all of this information out so the app can integrate with whatever service the end user cares to use and not just B2C? I would assume such a solution would require changes to both the Angular 2 pages and the API.
I have an action method that uses my authentication filter:
public class TutorAuthenticationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var req = filterContext.HttpContext.Request;
var auth = req.Headers["Authorization"];
if (!string.IsNullOrEmpty(auth))
{
var cred = System.Text.Encoding.ASCII.GetString(Convert.FromBase64String(auth.Substring(6))).Split(':');
var user = new { Name = cred[0], Password = cred[1] };
if (userService.AuthorizeTutor(user.Name, user.Password))
{
return;
}
}
filterContext.HttpContext.Response.AddHeader("WWW-Authenticate", $"Basic realm= {BasicRealm}");
filterContext.Result = new HttpUnauthorizedResult();
}
}
I would like to then display on main page something for user that have been authenticated this way, but this does not work in my View :(
#if (Request.IsAuthenticated)
{
<h1>Hello</h1>
}
I know it does not work because I don't use Identity, but is there any way that I can do this?
Thank you for answers :)
I suppose, that sending login and password in header is not secure. Better solution is one time when user is verified. And after checking, you can check all request.
For example, if you use FormsAuthentication and authCookie it's very simple:
Set auth mentod in web.config: <authentication mode="Forms" />
When login and password is valid, use FormsAuthentication.SetAuthCookie(userName, createPersistentCookie = true); This step is performed only once, when user login to application.
Then you can use property this.Request.IsAuthenticated in view or HttpContext.Current.Request.IsAuthenticated in controller (or filter).
And it works attribute [Authorize] on conntrolers or actions (public methods in conntrollers). When request is not authenticated, request is redirected to default (or set in web.config) login page.
Create a new extension method for the request object say (IsUserAuthenticated()) & in that method check if the user is valid.
Once this is done, you can use this new extension method the same way you are using Request.IsAuthenticated Property.
Below is the sample code, which you will need to tweak as per your needs. (specifically for
userservice
initialization)
public class RequestValidator
{
public bool IsValid(HttpRequest request)
{
bool isValid = false;
//TODO: Intitialize your userService here, may be using DI or a concrete object creation depending on your implementation
var auth = request.Headers["Authorization"];
if (!string.IsNullOrEmpty(auth))
{
var cred = System.Text.Encoding.ASCII.GetString(Convert.FromBase64String(auth.Substring(6))).Split(':');
var user = new { Name = cred[0], Password = cred[1] };
isValid = userService.AuthorizeTutor(user.Name, user.Password))
}
return isValid;
}
}
Your attribute will change like this
public class TutorAuthenticationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var req = filterContext.HttpContext.Request;
RequestValidator validator = new RequestValidator();
if(validator.IsValid(request))
{
return;
}
filterContext.HttpContext.Response.AddHeader("WWW-Authenticate", $"Basic realm= {BasicRealm}");
filterContext.Result = new HttpUnauthorizedResult();
}
}
And the extension method to be used on view will be
public static class Extensions
{
public static bool IsUserAuthenticated(this HttpRequest request)
{
RequestValidator validator = new RequestValidator();
return validator.IsValid(request);
}
}
Use it like this
#if(Request.IsUserAuthenticated())
{
<p>Hello</p>
}
If you want to pass the boolean value indicating if the user is authenticated, maybe it makes sense to just use the model object and pass it to the view.
Or maybe you should review your Form Authentication to make Request.IsAuthenticated working properly. This thread will help to start digging.
Another option would be to consider using the IAuthorizationFilter instead of the custom action filter. This thread will be a starting point.
Hope that helps!
To meet your purpose, you would need to set HttpContext.User to some valid IPrincipal.
So, if according to your criteria, the user is valid you just need to create a GenericPrinicpal and set HttpContext.User with the instance you have just created.
Something like this:
var genericIdentity=new GenericIdentity(user.Name, "CustomAuthType");
var genericPrincipal=new GenericPrincipal(genericIdentity, null);
HttpContext.User = genericPrincipal;
With GenericIdentity, the value of IsAuthenticated is dependent on the Name property, so as soon as the GenericIdentity has a Name, it is considered to be authenticated.
In this example, I'm setting the HttpContext.User and not the Thread.CurrentPrincipal so that you can get the IsAuthenticated from the Request.IsAuthenticated property.
Some extra and related information:
GenericIdentity Class
Principal and Identity Objects
Create GenericPrincipal and GenericIdentity Objects
Replacing a Principal Object
in your startup.cs file add this
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Login"),
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(40)
});
I'm using an MVC system where I use OWIN authentication, implementing my own IUserStore/etc and that all works great. I'm not looking into linking my logins with external logins, and i'm having trouble using the code that comes with MVC5. In particular:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LinkLogin(string provider)
{
// Request a redirect to the external login provider to link a login for the current user
return new ChallengeResult(provider, Url.Action("LinkLoginCallback", "Manage",), User.Identity.GetUserId());
}
and
public class ChallengeResult : HttpUnauthorizedResult
{
// Used for XSRF protection when adding external logins
internal const string XsrfKey = "XsrfId";
public ChallengeResult(string provider, string redirectUri)
: this(provider, redirectUri, null)
{
}
public ChallengeResult(string provider, string redirectUri, string userId)
{
LoginProvider = provider;
RedirectUri = redirectUri;
UserId = userId;
}
public string LoginProvider { get; set; }
public string RedirectUri { get; set; }
public string UserId { get; set; }
public override void ExecuteResult(ControllerContext context)
{
context.RequestContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
var properties = new Microsoft.Owin.Security.AuthenticationProperties() { RedirectUri = RedirectUri };
if (UserId != null)
{
properties.Dictionary[XsrfKey] = UserId;
}
Container.GetInstance<Microsoft.Owin.Security.IAuthenticationManager>().Challenge(properties, LoginProvider);
}
}
I have only changed the challenge result so that it uses my structuremap DI instead of GetOwinContext for the authenticationmanager. The problem is that i have all of my controllers set with a [CustomAuthorize] attribute, and when i reach the LinkLogin method, it works fine. The context.user.identity variable is of AuthenticationType 'ApplicationCookie', which is good and what i want.
However when it goes through the challegeresult part of 'LinkLogin', i hit my CustomAuthorize code again and now the user is no longer the same. The Context.User.Identity variable is now of AuthenticationType 'Negotiate', and it has my windows login as the username instead.
This doesn't work with my application at all, and i end up getting a 403 error because it can't resolve that user to the correct Cookie user.
Is there something that i'm doing wrong here? I've tried to put AllowAnonymous above LinkLogin, but that doesn't seem to help at all. Not sure if i need to modify ChallengeResult somehow to fit my needs better. Any help would be great, thanks!
turns out i was passing a null value as the 'provider' by accident. once i fixed that, it worked correctly