For a school project I am remaking the Top2000 website (dutch website with the top 2000 most liked songs per year). Now I have a problem with the roles and the authorization.
I want to add an admin role and give only user with that role access to the privacy page.
This is what I got so far:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDbContext<db_a74225_top2000Context>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddControllersWithViews();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
}
HomeController.cs
namespace Top2000.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
return View();
}
[Authorize(Roles = "Admin")]
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
Screenshot of dbo.AspNetUserRoles
Screenshot of dbo.AspNetUsers
Screenshot of dbo.AspNetRoles
I expect to get to the privacy page but when I login I still get Access Denied.
Screenshot of Access denied page
Judging from the screenshot of your database, you may not have successfully created a role, you can create a user with a role like the following method CreateRolesandUsers.
public class HomeController : Controller
{
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<IdentityUser> _userManager;
public HomeController(RoleManager<IdentityRole> roleManager, UserManager<IdentityUser> userManager)
{
_roleManager = roleManager;
_userManager = userManager;
}
public async Task CreateRolesandUsers()
{
bool x = await _roleManager.RoleExistsAsync("Admin");
if (!x)
{
var role = new IdentityRole();
role.Name = "Admin";
await _roleManager.CreateAsync(role);
}
var user = new IdentityUser();
user.UserName = "123#123.com";
user.Email = "123#123.com";
string password = "Defaultpassword01!";
IdentityResult chkUser = await _userManager.CreateAsync(user, password);
if (chkUser.Succeeded)
{
var result = await _userManager.AddToRoleAsync(user, "Admin");
}
}
public IActionResult Index()
{
return View();
}
[Authorize(Roles = "Admin")]
public IActionResult Privacy()
{
return View();
}
}
Your DbContext:
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
Then in your startup change your code
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
to
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
When you access this method to successfully create a role, you can log in to the user and then access Privacy.
Related
I have a UserService in my ASP.NET Core project of my Blazor web assembly solution (.NET 6) in which I'm trying to access the current logged in user to determine the ID of the company they work for.
UserService.cs
public class UserService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IHttpContextAccessor _httpContextAccessor;
public UserService(UserManager<ApplicationUser> userManager, IHttpContextAccessor httpContextAccessor)
{
_userManager = userManager;
_httpContextAccessor = httpContextAccessor;
}
private ClaimsPrincipal User => _httpContextAccessor.HttpContext?.User;
public IEnumerable<UserDto> GetUsers()
{
var users = new List<UserDto>();
var companyId = GetCompanyId();
users = _userManager.Users.Select(u => new UserDto()
{
CompanyId = u.CompanyId,
Email = u.Email,
FirstName = u.FirstName,
LastName = u.LastName,
Id = u.Id,
DeleteDate = u.DeleteDate
}).Where(u => u.DeleteDate == null && u.CompanyId == companyId.Result).ToList();
return users;
}
public async Task<int> GetCompanyId()
{
var user = await _userManager.GetUserAsync(User);
return user.CompanyId;
}
}
As you can see I've injected IHttpContextAccessor into the constructor for my UserService and I've also registered builder.Services.AddHttpContextAccessor(); in my Program.cs file (code also shown below).
Whenever my code hits the return user.CompanyId; line of my UserService, it errors with a null reference exception as the user is always null. I'm logged in and successfully authenticated by the point I try and get it to run this code.
Am I missing something from my Program.cs file or I have done something wrong in the UserService itself?
Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddRoles<ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
.AddProfileService<ProfileService>();
builder.Services.AddAuthentication()
.AddIdentityServerJwt();
builder.Services.AddTransient<IUnitOfWork, UnitOfWork>();
builder.Services.AddTransient<UserService>();
builder.Services.AddScoped<DialogService>();
builder.Services.AddControllersWithViews();
builder.Services.AddHttpContextAccessor();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor().AddCircuitOptions(options => { options.DetailedErrors = true; });
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapBlazorHub();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.Run();
to get userId by using contextAccessor try below code, instead of usermanager
var userId=_httpContextAccessor.User.Identity.Name
In my current setup I am using a SPA (ReactJS) running on a different machine than my ASP.net Core Server. The whole client-server communication runs through SignalR except authentication. For security I want to use cookie-based authentication. As user-management I am using ASP.net.Core.Identity.
Setting up CORS works and now I'm implementing a login/logout mechanism based on simple HTTP-Request.
Here the workflow:
Send SignIn-Request to server via simple HTTP. (Works)
After successfully signin set cookie on client. (Works)
Client starts SignalR hub-connections (using the provided cookie). (Works)
Client sends data to server via HubConnetion. (Works)
Server sends data based on Claimspricipal. (Fails)
After the successfull authentication the client receives the cookie and uses it when negotiating with the signalR-hubs. When calling other controller-endpoints I have access to the ClaimsPrincipal which authenticated previously.
Now to my problem: when accessing the HubCallerContext the ClaimsPrincipal is not set (identity nor claims are set). Do I need to register the ClaimsPrincipal on this context as well or will it be handled by ASP.net? Other examples about authentication are assumuming that caller and hub are running on the same server. Did I missed or missunderstood something?
Here my Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<SignInManager<ApplicationUser>, ApplicationSignInManager>();
services.AddScoped<IAuthorizationHandler, MyRequirementHandler>();
services.AddCors(
options => options.AddPolicy("CorsPolicy",
builder =>
{
builder
.SetIsOriginAllowed(origin =>
{
if (string.IsNullOrEmpty(origin)) return false;
if (origin.ToLower().StartsWith("http://localhost")) return true;
return false;
})
.AllowAnyHeader()
.AllowCredentials();
})
);
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
services.AddAuthorization(options =>
{
options.AddPolicy("PolicyName",
policy => { policy.Requirements.Add(new MyRequirement()); });
});
services.ConfigureApplicationCookie(config =>
{
config.Cookie.HttpOnly = false;
config.Cookie.Name = "my-fav-cookie";
config.ExpireTimeSpan = TimeSpan.FromDays(14);
config.Cookie.SameSite = SameSiteMode.None;
config.Cookie.SecurePolicy = CookieSecurePolicy.Always;
config.SlidingExpiration = false;
});
services.AddSignalR(opt => { opt.EnableDetailedErrors = true; })
.AddMessagePackProtocol(option => { option.SerializerOptions.WithResolver(StandardResolver.Instance); })
.AddNewtonsoftJsonProtocol(option =>
{
option.PayloadSerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
option.PayloadSerializerSettings.DateParseHandling = DateParseHandling.DateTime;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerManager logger,
ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("CorsPolicy");
app.UseRouting();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
...some Hubendpoints...
});
}
Here the MyRequirement.cs:
public class MyRequirement : IAuthorizationRequirement
{
}
And here the MyRequirementHandler.cs:
public class
MyRequirementHandler: AuthorizationHandler<MyRequirement,
HubInvocationContext>
{
private readonly IAuthorizationHelper _authorizationHelper;
private readonly UserManager<ApplicationUser> _userManager;
public MyRequirementHandler(
IAuthorizationHelper authorizationHelper,
UserManager<ApplicationUser> userManager)
{
_authorizationHelper = authorizationHelper;
_userManager = userManager;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
MyRequirement requirement,
HubInvocationContext resource)
{
var user = context.User;
...
}
}
And the Hub.cs:
public class Hub: Hub<ISystemClient>
{
private readonly ISystemService _systemService;
public Hub(ISystemService systemService)
{
_systemService = systemService;
}
[Authorize("PolicyName")]
public async Task DoSthFancy()
{
var user = Context.User;
var fancyStruff = await _systemService.GetFancyStuff(user);
await Clients.Caller.SendFancyStuff(fancyStuff);
}
}
Thanks for your help!
EDIT (04-11-2022)
I edited/fixed the code-snippet of the Startup.cs due to bad copy-paste. Thanks for the hint.
After implementing an example step by step I figured out that ASP sets the ClaimsPrincipal inside the HubContext only when you are challeging the hub-endpoint or the hub itself by adding a [Authorization] attribute. So what was missing was a implementation of an AuthenticationHandler, a registration in the configuration and setting the authorization-attribute.
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public CustomAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager) : base(options, logger, encoder, clock)
{
_userManager = userManager;
_signInManager = signInManager;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Context.Request.Cookies.TryGetValue("my-fav-cookie", out var cookie))
{
var user = Context.Request.HttpContext.User;
var ticket = new AuthenticationTicket(user, new(), CustomCookieScheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
return Task.FromResult(AuthenticateResult.Fail("my-fav-cookie"));
}
}
In the Startup.cs you need to register the Authentication:
services.AddAuthentication().AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>(
"MyCustomScheme", _ => { });
and in the Hub:
namespace My.HubProject {
[Authorize(AuthenticationScheme="MyCustomScheme")]
public class MyHub : Hub {
...
}
}
Hope it will help others!
I am building authentication on my asp.net core application using the identity. I followed 2 different tutorials but in both cases, my app doesn't send cookies to the browser. I reckon I am missing something important but I don't know what. Could you point me in the right direction? Here is the relevant code:
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContextPool<IdentityDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityDbContext>();
services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
public class HomeController : Controller
{
private readonly SignInManager<IdentityUser> _signInManager;
private readonly UserManager<IdentityUser> _userManager;
public HomeController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
{
_signInManager = signInManager;
_userManager = userManager;
}
public async Task<IActionResult> SignUp()
{
await _userManager.CreateAsync(new IdentityUser("my_name"), "Secret123:");
return Ok("Signed up");
}
public async Task<IActionResult> SignIn()
{
var result = await _signInManager.PasswordSignInAsync("my_name", "Secret123:", true, false);
if(!result.Succeeded)
return Ok("Sign in failed");
return Ok("Signed in");
}
}
Edit: I created a fresh project and pasted these 2 classes, installed the same NuGet packages there, and the cookies worked. I have a problem somewhere else in the old project. I still have no clue where. I would appreciate if you comment where you think it might be.
I have this code in ApplicationClaimsPrincipalFactory on ASP.Net MVC Core 2.2:
public class ApplicationClaimsPrincipalFactory : UserClaimsPrincipalFactory<TblUsers, TblOrganizationChart>
{
public static readonly string PhotoFileName = nameof(PhotoFileName);
private readonly IOptions<IdentityOptions> _optionsAccessor;
private readonly IApplicationRoleManager _roleManager;
private readonly IApplicationUserManager _userManager;
public ApplicationClaimsPrincipalFactory(
IApplicationUserManager userManager,
IApplicationRoleManager roleManager,
IOptions<IdentityOptions> optionsAccessor)
: base((UserManager<TblUsers>)userManager, (RoleManager<TblOrganizationChart>)roleManager, optionsAccessor)
{
_userManager = userManager;
_userManager.CheckArgumentIsNull(nameof(_userManager));
_roleManager = roleManager;
_roleManager.CheckArgumentIsNull(nameof(_roleManager));
_optionsAccessor = optionsAccessor;
_optionsAccessor.CheckArgumentIsNull(nameof(_optionsAccessor));
}
public override async Task<ClaimsPrincipal> CreateAsync(TblUsers user)
{
var principal = await base.CreateAsync(user);
addCustomClaims(user, principal);
return principal;
}
private static void addCustomClaims(TblUsers user, IPrincipal principal)
{
((ClaimsIdentity)principal.Identity).AddClaims(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.Integer),
new Claim(ClaimTypes.GivenName, user.TblPerson.FullName ?? string.Empty)
});
}
}
when executing var principal = await base.CreateAsync(user); line I have this exception:
StackTrace = " at System.Security.Claims.Claim..ctor(String type, String value, String valueType, String issuer, String originalIssuer, ClaimsIdentity subject, String propertyKey, String propertyValue)\r\n at System.Security.Claims.Claim..ctor(String type, String va...
I guest base is null but I don't know what I need to do.
Startup.cs:
`public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.Configure<SiteSettings>(options => Configuration.Bind(options));
IdentityServicesRegistry.AddCustomServices(services);
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMemoryCache();
services.AddDbContext<AbfaContext>(options =>
options
.UseSqlServer(Configuration.GetConnectionString("Default")));
services.AddDbContext<AbfaContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("AbfaConnectionString"), x => x.UseNetTopologySuite()));
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddMvc(options =>
{
options.UseYeKeModelBinder();
options.AllowEmptyInputInBodyModelBinding = true;
options.EnableEndpointRouting = false;
}).AddNewtonsoftJson(options =>
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver())
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization()
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
services.AddDNTCommonWeb();
services.AddHttpContextAccessor();
var thisServiceProvider = services.BuildServiceProvider();
var accessor = thisServiceProvider.GetService<IHttpContextAccessor>();
AddLog.SetHttpContextAccessor(accessor);
return thisServiceProvider;
}
public void Configure(ILoggerFactory loggerFactory, IApplicationBuilder app, IHostingEnvironment env)
{
loggerFactory.AddDbLogger(serviceProvider: app.ApplicationServices, minLevel: LogLevel.Warning);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
app.UseHttpsRedirection();
}
app.UseFileServer(new FileServerOptions
{
EnableDirectoryBrowsing = false
});
app.UseStaticFiles();
app.UseCustomServices();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Account}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}`
I am trying to find a way to prevent my aspnetcore application to add "?ReturnUrl=" to the URL. Does anyone know how to do it, using some kind of middleware.
I tried doing it like below but it did not have any effect:
public class RequestHandlerMiddleware
{
private readonly RequestDelegate _next;
public RequestHandlerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if(context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("?ReturnUrl="))
{
context.Request.QueryString = new QueryString(string.Empty);
}
await _next.Invoke(context);
}
}
public static class RequestHandlerMiddlewareExtension
{
public static IApplicationBuilder UseRequestHandlerMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestHandlerMiddleware>();
}
}
Registration in startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseAuthentication();
app.UseRequestHandlerMiddleware();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
}
Lastly, I have also tried some (tweaked) approaches from the older post regarding the same issue for .NET frameworks here (on stackoverflow) but also failed
Edit: I am not using any additional AuthorizationAttribute / Handler other then the 'standard' [Authorize] attribute. Only:
services.AddAuthorization();
Edit 2: I totally forgot that I also register a portion of the startup elsewhere in the application since it is shared:
public static IServiceCollection Load(IServiceCollection services, IConfiguration config)
{
services.AddDbContext<SqlContext>(options =>
{
options.UseSqlServer(config.GetConnectionString("DefaultConnection"));
});
services.AddIdentity<User, Role>(options =>
{
options.Lockout = new LockoutOptions
{
AllowedForNewUsers = true,
DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30),
MaxFailedAccessAttempts = 5
};
})
.AddEntityFrameworkStores<SqlContext>()
.AddDefaultTokenProviders()
.AddUserStore<UserStore<User, Role, SqlContext, Guid>>()
.AddRoleStore<RoleStore<Role, SqlContext, Guid>>()
.AddUserManager<ApplicationUserManager>();
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 5;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = true;
});
services.ConfigureApplicationCookie(options =>
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api") &&
ctx.Response.StatusCode == (int)HttpStatusCode.OK)
{
ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else if (ctx.Response.StatusCode == (int)HttpStatusCode.Forbidden)
{
ctx.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
return Task.FromResult(0);
}
});
return services;
}
The first thing that comes to mind is :
[HttpGet]
public IActionResult LogIn()
{
if (!string.IsNullOrEmpty(Request.QueryString.Value))
return RedirectToAction("Login");
return View();
}
Which will remove QueryString part from the URL so that "ReturnUrl" will not stay on user address-bar for long and will reject any QueryString.
Better workaround would be creating your own version of AuthorizeAttribute which will not put a ReturnUrl in QueryString but it seems with new policy based authorization approach coming around, customizing AuthorizeAttribute is discouraged. more
It might be also possible with policy based approach and creating a custom AuthorizationHandler.
(I will post an update as soon as I try it out)