I am pretty new to .net core development and I've got a problem with my .net core web application regards the user authentication by cookie authentication scheme. The problem is, that even tough the cookie is created and I'm logged in properly, I am not able to automatically re-login if the user calls the login page again.
Here is my code for better understanding:
Services configuration
services.AddAuthentication()
// Add operator authentication
.AddCookie("OPERATOR_AUTH", o =>
{
o.LoginPath = "/login";
o.LogoutPath = "/logout";
o.AccessDeniedPath = "/denied";
// Block js cookie access
o.Cookie.HttpOnly = true;
// Limit cookies to https connections in production environments
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
// Only allow the cookie on the service pages
o.Cookie.SameSite = SameSiteMode.Strict;
});
services.AddTransient<ILoginService, LoginService>();
App configuration
app.UseCookiePolicy();
app.UseAuthentication();
app.Use(next => ctx =>
{
var cookieOptions = new CookieOptions
{
HttpOnly = true
};
var tokens = antiforgery.GetAndStoreTokens(ctx);
ctx.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,cookieOptions);
return next(ctx);
});
Login Controller
/// <summary>
/// The operator login service
/// </summary>
private readonly ILoginService _loginService;
/// <summary>
/// The logger instance received by dependency
/// injection
/// </summary>
private readonly ILogger<LoginController> _logger;
/// <summary>
/// Renders the login view
/// </summary>
/// <returns></returns>
[Route("login")]
public async Task<IActionResult> Login()
{
return View(new OperatorLoginModel());
}
/// <summary>
/// Renders the login denied view
/// </summary>
/// <returns></returns>
[Route("denied")]
public IActionResult Denied()
{
return View();
}
/// <summary>
/// Performs a login
/// </summary>
/// <param name="model">The login model</param>
/// <returns>The result view</returns>
[Route("login")]
[ValidateAntiForgeryToken]
[HttpPost]
public async Task<IActionResult> Login(OperatorLoginModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var op = await _loginService.IsAuthorizedOperator(model.OperatorCode, model.OperatorPassword);
if (op != null)
{
return await SignInOperator(op);
}
return RedirectToAction("Denied", "Login");
}
/// <summary>
/// Performs a logout
/// </summary>
/// <returns>The result view redirection</returns>
[Route("logout")]
[ValidateAntiForgeryToken]
[HttpPost]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync("OPERATOR_AUTH");
return RedirectToAction("Index", "Home");
}
/// <summary>
/// Performs the actual sign in by assigning the operator
/// identity to the http context
/// </summary>
/// <param name="op">The operator to sign in</param>
/// <returns>The result view redirection</returns>
private async Task<IActionResult> SignInOperator(Operator op)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier,op.Code),
new Claim(ClaimTypes.Name, op.Description)
};
var authProperties = new AuthenticationProperties()
{
AllowRefresh = true,
IsPersistent = true,
ExpiresUtc = DateTimeOffset.Now.AddMinutes(30).UtcDateTime,
IssuedUtc = DateTimeOffset.Now.UtcDateTime
};
var identity = new ClaimsIdentity(claims, "OPERATOR_AUTH");
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("OPERATOR_AUTH", principal, authProperties);
return RedirectToAction("UserInformation", "Home");
}
/// <summary>
/// Constructor of the login controller
/// </summary>
/// <param name="logger">Logger instance</param>
/// <param name="loginService">Login service</param>
public LoginController(ILogger<LoginController> logger, ILoginService loginService)
{
_logger = logger;
_loginService = loginService;
}
What happens is the following:
The operator can correctly navigate to the login view
The operator is able to login by entering his or her credentials
The operator is navigated to the user information view
The cookie is set in the browser:
Now if I log off, the cookie is deleted correctly.
If I don't log of, but refresh the page or renavigate to Home/Login I am not able to receive the User information in the HttpContext:
Also no automatic authentication is done.
Am I missing something? Anything wrong here?
The problem could be solved by adding the authorization context attribute, aswell as the anonymous context attribute to the controller method as mentioned by kirk larkin.
[Route("login")]
[AllowAnonymous]
[Authorize(AuthenticationSchemes = "OPERATOR_AUTH")]
public IActionResult Login()
{
if (HttpContext.User.Identity.IsAuthenticated)
{
return RedirectToAction("UserInformation", "Home");
}
return View(new OperatorLoginModel());
}
Related
I'm Trying to access Cookies from Static Class in Asp.net Core , So far I couldn't find any useful topics on Google. any Suggestions?
I already Did that in asp.net:
public static string GetString(string Key)
{
var format = System.Web.HttpContext.Current.Request.Cookies["Language"];
if (format == null)
format = new HttpCookie("Language") { Value = "Fa" };
ResourceManager rm = new ResourceManager("Ippv.Virtual.Application.Properties." + format.Value,
Assembly.GetExecutingAssembly());
return rm.GetString(Key);
}
How can I write equivalent of this code in asp.net core?
In ASP.NET, we can access cookies using httpcontext.current but in ASP.NET Core, there is no htttpcontext.currently. In ASP.NET Core, everything is decoupled and modular.Httpcontext is accessible from Request object and the IHttpContextAccessor interface which is under "Microsoft.AspNetCore.Http" namespace and this is available anywhere in the application. Refer to the following code:
public class TestController : Controller
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TestController(IHttpContextAccessor httpContextAccessor)
{
this._httpContextAccessor = httpContextAccessor;
}
public IActionResult Index()
{
//read cookie from IHttpContextAccessor
string cookieValueFromContext = _httpContextAccessor.HttpContext.Request.Cookies["key"];
//read cookie from Request object
string cookieValueFromReq = Request.Cookies["Key"];
//set the key value in Cookie
Set("kay", "Hello from cookie", 10);
//Delete the cookie object
Remove("Key");
return View();
}
/// <summary>
/// Get the cookie
/// </summary>
/// <param name="key">Key </param>
/// <returns>string value</returns>
public string Get(string key)
{
return Request.Cookies[key];
}
/// <summary>
/// set the cookie
/// </summary>
/// <param name="key">key (unique indentifier)</param>
/// <param name="value">value to store in cookie object</param>
/// <param name="expireTime">expiration time</param>
public void Set(string key, string value, int? expireTime)
{
CookieOptions option = new CookieOptions();
if (expireTime.HasValue)
option.Expires = DateTime.Now.AddMinutes(expireTime.Value);
else
option.Expires = DateTime.Now.AddMilliseconds(10);
Response.Cookies.Append(key, value, option);
}
/// <summary>
/// Delete the key
/// </summary>
/// <param name="key">Key</param>
public void Remove(string key)
{
Response.Cookies.Delete(key);
}
I have tried to migration my project from v2 to v3 and I think i have updated everything, but for some reason i can login fine and i get an access_token returned, but this access_token fails to authorize a request. Not sure why, but if someone can see anything obvious that would be great. My application is a web Api with a angular2 web application for the client.
In v2 i was using JWT, but i understand that this is the default in v3 so i may have some code that needs to removed for this to work.
To restrict a controllers methods or a single method, i am using [Authorize] attribute.
Extension method to be called in Startup.cs for adding Authentication, done to keep Startup.cs file from getting to big and keeps things in one place
/// <summary>
/// Add authentication
/// </summary>
/// <param name="services"></param>
/// <param name="appSettings"></param>
public static void AddAuthentication(this IServiceCollection services, AppSettings appSettings)
{
// appSettings.ApiUrl = "http://localhost:5000"
// appSettings.WebsiteUrl = "http://localhost:4200"
// the default value for AllowuserNameCharacters is "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+"
// here we have just added some additional characters
services.AddIdentity<User, Role>(options => { options.User.AllowedUserNameCharacters += "'&"; })
.AddDefaultTokenProviders();
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
// return unauthorized message instead of url
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return System.Threading.Tasks.Task.CompletedTask;
};
});
// configure all tokens generated from aspnet to expire in 3 days (create password, forget password etc)
services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromDays(3));
// Authentication
var authenticationBuilder = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.Authority = appSettings.ApiUrl;
cfg.Audience = appSettings.ApiUrl;
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
});
// OpenIddict
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFramework(e => e.UseDbContext<ApplicationDbContext>());
})
.AddServer(options =>
{
// For token lifetimes look at the below link
// https://github.com/openiddict/openiddict-core/wiki/Configuration-and-options
// Enable the authorization, logout, token endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token");
// Note: the Mvc.Client sample only uses the code flow and the password flow, but you
// can enable the other flows if you need to support implicit or client credentials.
options.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(Scopes.Email,
Scopes.Profile,
Scopes.OpenId,
Scopes.OfflineAccess,
Scopes.Roles);
// code to allow requests without client_id
options.AcceptAnonymousClients();
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.DisableTransportSecurityRequirement(); // Never use https because we use load balancer
})
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
}
Startup.cs
/// <summary>
/// Configure services
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
// application settings
var appSettings = new AppSettings();
Configuration.Bind(appSettings);
// mvc
services.AddMvc(appSettings);
// identity / saml etc
services.AddAuthentication(appSettings);
}
/// <summary>
/// Configure application
/// </summary>
/// <param name="applicationBuilder"></param>
/// <param name="webHostEnvironment"></param>
/// <param name="loggerFactory"></param>
public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment webHostEnvironment, ILoggerFactory loggerFactory)
{
// get current appSettings and output to logger
var appSettings = applicationBuilder.ApplicationServices.GetRequiredService<AppSettings>();
var logger = loggerFactory.CreateLogger(appSettings.ApiUrl);
logger.LogInformation(string.Format("{0}\n{1}", appSettings.Environment, appSettings.Data.DefaultConnection.ConnectionString));
// environment
webHostEnvironment.EnvironmentName = appSettings.Environment;
// CORS
applicationBuilder.UseCors("Default");
// static files in root
applicationBuilder.UseStaticFiles();
// routing
applicationBuilder.UseRouting();
// comment this out and you get an error saying
// InvalidOperationException: No authentication handler is configured to handle the scheme: Microsoft.AspNet.Identity.External
applicationBuilder.UseAuthentication();
// for authorization headers
applicationBuilder.UseAuthorization();
// response caching
applicationBuilder.UseResponseCaching();
// routes
applicationBuilder.UseEndpoints(options =>
{
// default goes to Home, and angular will deal with client side routing
options.MapControllerRoute(
name: "default",
pattern: "{*url}",
defaults: new { controller = "home", action = "index" });
});
}
I have updated my AuthorizationController.cs as well
/// <summary>
/// Handles authorization requests
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class AuthorizationController : Controller
{
private IOptions<IdentityOptions> _identityOptions;
private OpenIddictApplicationManager<OpenIddictEntityFrameworkApplication> _applicationManager;
private SignInManager<MyUser> _signInManager;
private UserManager<MyUser> _userManager;
private AppSettings _appSettings;
private IEncryptionService _encryptionService;
/// <summary>
/// Create a new authorization controller
/// </summary>
/// <param name="identityOptions"></param>
/// <param name="applicationManager"></param>
/// <param name="signInManager"></param>
/// <param name="userManager"></param>
/// <param name="appSettings"></param>
/// <param name="encryptionService"></param>
public AuthorizationController(
IOptions<IdentityOptions> identityOptions,
OpenIddictApplicationManager<OpenIddictEntityFrameworkApplication> applicationManager,
SignInManager<MyUser> signInManager, UserManager<MyUser> userManager,
AppSettings appSettings, IEncryptionService encryptionService)
{
_identityOptions = identityOptions;
_applicationManager = applicationManager;
_signInManager = signInManager;
_userManager = userManager;
_appSettings = appSettings;
_encryptionService = encryptionService;
}
// Note: to support interactive flows like the code flow,
// you must provide your own authorization endpoint action:
/// <summary>
/// Authorize an openId request
/// </summary>
/// <param name="connectRequest"></param>
/// <returns></returns>
[Authorize, HttpGet, Route("~/connect/authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId, new System.Threading.CancellationToken());
if (application == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidClient,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Details concerning the calling client application cannot be found in the database"
}
)
);
}
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel
{
ApplicationName = application.DisplayName,
RequestId = request.RequestId,
Scope = request.Scope
});
}
/// <summary>
/// Accept an openId request
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[Authorize, HttpPost("~/connect/authorize/accept"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the profile of the logged in user
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ServerError,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "An internal error has occurred"
}
)
);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
// returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
/// <summary>
/// Deny an openId request
/// </summary>
/// <returns></returns>
[Authorize, HttpPost("~/connect/authorize/deny"), ValidateAntiForgeryToken]
public IActionResult Deny()
{
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
/// <summary>
/// Logout
/// </summary>
/// <returns></returns>
[HttpPost("~/connect/logout")]
public async Task<IActionResult> Logout()
{
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
// Note: to support non-interactive flows like password,
// you must provide your own token endpoint action:
/// <summary>
/// Exchange request for valid openId token
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid"
}
)
);
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password))
{
if (_userManager.SupportsUserLockout)
{
await _userManager.AccessFailedAsync(user);
}
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid"
}
)
);
}
if (_userManager.SupportsUserLockout)
{
await _userManager.ResetAccessFailedCountAsync(user);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
else if (request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/refresh token.
var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// Retrieve the user profile corresponding to the refresh token
var user = await _userManager.GetUserAsync(info.Principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The refresh token is no longer valid"
}
)
);
}
// Ensure the user is still allowed to sign in
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in"
}
)
);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new NotImplementedException("The specified grant type is not implemented");
}
/// <summary>
/// Creates a principal based on the openId request
/// </summary>
/// <param name="request"></param>
/// <param name="user"></param>
/// <param name="properties"></param>
/// <returns></returns>
private async Task<ClaimsPrincipal> CreatePrincipalAsync(OpenIddictRequest request, MyUser user, AuthenticationProperties properties = null)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
if (!request.IsRefreshTokenGrantType())
{
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
principal.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Email,
Scopes.Profile,
Scopes.OfflineAccess,
Scopes.Roles
}.Intersect(request.GetScopes()));
}
// Set resource
principal.SetResources(new string[] { _appSettings.ApiUrl });
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return principal;
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
Also here is my applicationDbContext
/// <summary>
/// Application context to hold our openIddict entities
/// </summary>
public class ApplicationDbContext : DbContext, IApplicationDbContext
{
/// <summary>
/// Create context with connection string
/// </summary>
/// <param name="connectionString"></param>
public ApplicationDbContext(string connectionString) : base(connectionString)
{
}
/// <summary>
/// When creating models
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.UseOpenIddict();
}
/// <summary>
/// Authorizations
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkAuthorization> Authorization { get; set; }
/// <summary>
/// Applications
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkApplication> Application { get; set; }
/// <summary>
/// Tokens
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkToken> Token { get; set; }
/// <summary>
/// Scopes
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkScope> Scope { get; set; }
}
As i said, i am getting a token returned, like below
{
"access_token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI5NDVDNjFERjcyRUU1OUNDNUNFQzQ0NzJCRDE0MDg4NDEyNTAyRjM3IiwidHlwIjoiYXQrand0In0.TxmA-Lv60Lwvr5eXdu2fgG95Bx4AUoPvp5XniqSYbBgmpDrVXCUpSjnEv7tWQ5QLPjks6TAy8hGAGfgQPFB1xDM05mZyu2WDPBsoEQ2VtxuabaPhj-uK2KjnWEccjS-n-YAY7OlSDFXrmiDsFCOf3jaPGZ43gUCPyTVl_WVE_KBGVXa19RDZFf9Ger-TsHY3evYbsUchOKMco-CD2IGt6Xg6DttxFcKF3SUPwMXhn6AGdkNesGVIoVSeDd_CnM3MyHtJIbGbkRkmwpMWsbwaWGI38itUJ0XeUqn45B_GsZfaZllytuMHbuqt1GgL0gdOaCfBUxaqy7AaYsY9UKlaEw.LS4FnIOCRESdeo1c5K2_5Q.Ic8fssDScEu7lAomL8d_7JZMVho5hLphg269UIETZui7DUB2gund7YCtGq7imdnDtN-wsEoZaLHJ4UQSJrpuIMCN2pW69J9kKcx-UT1e2Ma-JNj2G5CtxwRf_bszRRsgWC1ia85LU5TPsUFvhd_wDxyUax2GKowb0FVl8EFRoFdZF40h06LaofxDzD2BFdwSYaHaDm3icVNZ0CRpYCoZq-MK_c3Fl98l57zjZl1CNscs1w0trApMDvQ7UeFcez3zelN24H6TCaXqTRP3lixMiv9Rtm3Kqkv67HEUAFD-vXIMyWQTo28oJMMAQz9zQeTp88JsfI4Pv7euUECEkwK7Fe5rbxhD4oicNNa_wmRo2vsjrpm4C7mmRKH2u3cX2CCTwahOsVHxu4kO3zliWmniW-krEAAsaW6BjoWqJiGN3ydoiHLIv3muM78jt85KzKxMIFsalpdf2F9Z8neQBglhzxZQFp9JiEecczxao6Y03WuFNX98kZh8Zr4D21CM_m634u3mf5-Gz-frVlSgnMrE9vgCG2eETI3fOnObf9pQ1XWpK7I9SE0AZvc2PciaKO4H7mKDqXuXFrzdiz6Tx9G9MMkXXbuKsuslZJ6q9wlE3xOl9mzd6shN9GvfSTQNrFoV-PS62xhnfCFKQDfYLzWF1fhQbq_RECp6IVJN960jaMAGIZ0yzIbRInIkbhdFEIbl__h9AYo6FfcZgYv4tfw-iB_unezgHCIPpbBPC0eRMiZh1wqpfwpiJ8zjvcXJXXCfSlu13KlBWD9tHVEHh6aHE258hGdFzApA-MTCkjHwMRjVgmv_-Ed-xwk0hjBpRhiZv-0kNwKI72YJVgZMX9CMpFBg2CO5z5hHSlc0pO9pc5ovuCZ_dYYRiqqcIDUJ5Wl6dDt1JCPI084C4yssC7MC4e94OtvfRtrI096qMI34qrLWi-jn7UOMnWMUunNepQLwQ07DK-ubXsS-m0xvSxPYxtE6XM6QebmvCcXj__vELGscDu8mWmvP1Y0L9SoSpWlErHvlCPpkfVeMnkdP74cKTYgpXSQcGduqcdfU86leI9oUYgnrcPwvMgUq3jwgHGWn_0d4Bo8CsChWatYWmSNKN88h3WfASSl6SyqeVbSSYvIp-0HBGxGOwODgO-YsOlOgeKmm8oSIhnELVNMVEY1uiUZpJ9DGfrUUXKMw7aIz6LK_zH_HxlMiBam0fxgAQwRyYlO0AFmuxFO62KEHFEjdDSEO9pcUP03_RNqDfAX-IAV_EoFT7CwVpOZMUvFLo78S4xkq2ss3CFbkA3J4ioud88T5SUfslnsZmY1dJYtW4HhlGF7SKVMN6GSwckz1YhyaxqlQpMbMRFA0uCbkM6u41K0-_toRQejKDX5juqFwqK8.m0bmPYOuAYYdc6WfjDWP2UneysP1G3FqwlmOzWOrTnM",
"token_type": "Bearer",
"expires_in": 3599,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwMzJBRUJBQjc4NkNCM0ExRkI3Nzk1NjdDQTRGRjU1ODg3OTFCNDEiLCJ4NXQiOiJjREt1dXJlR3l6b2Z0M2xXZktUX1ZZaDVHMEUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxIiwibmFtZSI6InBvd2VydXNlckBtZWFzdXJlMmltcHJvdmUuY29tIiwib2lfYXVfaWQiOiIxMDJjZTE0NC02NzkyLTRlYzAtOTlhNy0xZDBlMzdmNzkyZTYiLCJhdF9oYXNoIjoiV3BibExxNFdNR2dwdHdGVVVSMktRdyIsIm9pX3Rrbl9pZCI6IjQyMzBlYmJkLWUyNzItNDJlZS1iOTZiLTQ1ZGJmMTJlZWJmMiIsImV4cCI6MTYwNjQ3NzgzOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwLyIsImlhdCI6MTYwNjQ3NjYzOX0.WZkB63ZfZuJfigNVabPegu-B8TvMeS1DmgRQJS151XGR08Pw-fcCldb35oM7ZW9oQenj7059BAZMI1EveHWNVWOEFpabebi7TccGRoR1YKqWSNWTBDwyQgGMyehVmze_TPgsSjAJA0y1f_xtF3-ImfVx5Tzlcjg4XAmAhV3MRd-fEobdGk5540uto5hZJ7ieHrV_7U_FF4NgVT5nSw92bkFNjUokmNgMBpDWelZUEXmsb3MFGDMnQkP0oTGXeIcy0nuuIKpr1Liza_cvv1JfICQnSUKw_u3zdqSbsbXtzGg9GfunEhXf1zSF5dxfbNpr2E4bsnIxGJ-mOYxBsOQDJA",
"refresh_token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI5NDVDNjFERjcyRUU1OUNDNUNFQzQ0NzJCRDE0MDg4NDEyNTAyRjM3IiwidHlwIjoib2lfcmVmdCtqd3QifQ.E5xUDcqMK15spCgqoM8YrOCn_32wBX_X_XsG_f1nKOR-TcDI2AKi07vX62uMmbO1bgclGSGNynEUTuPt7KATf9UHXwpytXAK3_BnLxQLz7NbYIlfrja9t5z3gCRIGOZ-5gHbHcU6RdJrDpC73_V7CXYIfkVhfWeeS30_3GtLa-BUu4Nlr9YL7K22KdR0LJQsK6SXLMKobsj6PLrrywHnClaw6dG_O_SDezC5M_eIo_ErdHFqUBwWYhUHyntijJ9ekH4A2SiY0LGaQD6QyvmUNPC7E-LtLVO78saJYAgGW-CkQbwTBF7b_vOoEr_mPrFnpytwLnIvxSQCFVjclH-vzA.RGNxh09nh1RWb6XU6jNZSg.VRwdO_5UrB7tqaMHXwt6qZN8VucSlNXnBNoBxcvisDvjWhBOlhrJxEidGQPBeiIpbrYrWQXyPfSgFrmZAlqYcF-3cXTl0W-xR4oW1ZqRf95eMs-YTA9-bR6n8P2pV7WeYky7wkwa-i3Vs9rzasYjK18LTU6v71xMNVN2K0Z2HRgTXxPg3U4HUgkVQpZp4qetyhozdOhGBV37igAHSZHFUSYg-dlstz3DtEX7IXvi7GQbM_fLKVlDLWYZOtX1yH366C9TIjZbxGP3h-qPqfKKDNP-IhD_x2IpymmiKllfhfLKb62JQzy_ci-ICXGvs0z_ZLZirmzVSEotqrcKmwe1rlqRXbymP8llIXe3AzjLonN3I_35bLeqZz1KQE93pkLOpJGAJuhJ4dc8a7wJ5kxTAs9_CCvb6UBvhRxInUyZ9PQq026RaYJvN9i--x8JxAAtH58h9Y5zgm_M4kFbEoGgZshyGZ8QSQrp4JpPrGeW9ElvkyEmrGvEpm6zmy_Y6tEF3lXFfrGJL6FPGD5_q8m9lGQ162OCmZMGg2Sy46Hne0SPv04TpM43F-m82BTzXxGRbpvOoNye7Pl9dKbdOBno1WEmaesL-R-W_8qogNszW5c-gbhQTUjcQ1J5yuVL4r29qN9xhVDPKqxPYPzInCr30m8SH41NI6WusFVJbNoqVs43wymGfQwuDfJacVCIfuT1Lx2VMdVl3nvCAGOnEwiUDufjqMaA-Nh-Row4QIwTHMGwfzxbFkQjRfwq_NjVU0LZlVDX2BgSETw2ak3KpgAGoeDlVVwmXuQpbvZ3wRHVh4a3k3_JVGf1dOiHbXVcWx984-EARKmW6gLjIhe2t5tzIJ9NQqBrdBrCvMmQXWfBiP00-YH31yDfYRF9tNZoojnkcrkOewHFUsEcFuayBzP5ySR6RgGVVE_zkEXTnBXxeiynzfJn0D3XthpRPosLJTJz91tdmvo6CxPWuaaZoUlXq_EXqFrGCb09f11Hs_mKu7T7pHMbJioTAvj1Jy8jzduMU3Rth-w0A8Md2gdeKmoCXW51lvpuyFsP4R8AdthhXGUbAk6rNJXdkaUkRyygYU8ZQWI2WCyroDnBRpEIaBtjYT1Uf4zXyTUZ3jicKe5Rjr08tmgAQd_3pNu1Wb2dBQgvcoqnwXcnLvoMTp0yk7Osae94Pqh_Oyz45kz_oS_7fR6WvZm-avBPYLmW92eQWiMG6glxPPe_Vnq9ghwjYjS4KEvyFaDqusDWmxBEDwEzLgBTF3p0R2saedTbwpG4Epcey_T18KEuklAWbCwz-fvV3ip-_wGUNe6cuAOeyuXcUHm18Le0knZt6xNS_a8cxMz9RAEpBs3a-tOWQLrVELXxnNY03NL2szkNQuDUzY9JRtTxqngxcPpHVk058dRG1rwFZqiI4-6_yL03X_fbXsIR0ItBig9surYyB_crwyH3C6OZTnGgwxLKUU9qY0LNTzR0gatrrT1l8NrorLAvODzQqhrqqClHgZkVoQXANvz3mNZWIpxjCkOgDkH-YOaaR3egnwLVMk_clo3-gC76UQ-5T-NnZ7MJTv2twFBhUKHABEJsT9a3nR7ra2CbHFzJFNvRRCPHAGOVAY-y-Ek0xZn8mvd3Fvw4wlej4EdQvlnFUcaIH4Cl0MZD9G9t2W3A96KLRZctVfGK6W1yM846DWHwSfeTj3ZPN_6bYDqbWHoXrQxe5BGi9aN8PXfL1Bu9W5jhQpgrvTRvnjhYqwN24ta9r6BbrMzBL3cszf56dG3Ko6aA8gDwUmDSjyNVhk76sdoW.1xLMuQf7NjeYjAXDL5BUxaoFSR0EECEzPh7XxkE8Hj4"
}
Here is the token being passed successfully, but Unauthorized is returned, as you can see the next call fails as well.
Any help much appreciated
I know this is old, but just post my solution in case anyone has the same issue.
Just specificy the schema for the Authorize attribute:
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
I am using OAuth to generate tokens for my application, specifically JWT. I have this code in my startup class:
private void ConfigureOAuthTokenGeneration(IAppBuilder app)
{
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(DatabaseContext.Create);
app.CreatePerOwinContext<UserService>(UserService.Create);
app.CreatePerOwinContext<RoleService>(RoleService.Create);
// Plugin the OAuth bearer JSON Web Token tokens generation and Consumption will be here
var OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
//For Dev enviroment only (on production should be AllowInsecureHttp = false)
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new OAuthProvider(),
AccessTokenFormat = new CustomJwtFormat("http://localhost:58127")
};
// OAuth 2.0 Bearer Access Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
}
As you can see I have set a custom OAuthProvider and for the AccessTokenFormat I am using a CustomJwtFormat. The OAuthProvider looks like this:
public class OAuthProvider : OAuthAuthorizationServerProvider
{
/// <summary>
/// Validate client authentication
/// </summary>
/// <param name="context">The current context</param>
/// <returns></returns>
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Validate all requests (because our front end is trusted)
context.Validated();
// Return nothing
return Task.FromResult<object>(null);
}
/// <summary>
/// Validate user credentials
/// </summary>
/// <param name="context">The current context</param>
/// <returns></returns>
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Allow any origin
var allowedOrigin = "*";
// Add the access control allow all to our headers
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
// Get our user service
var service = context.OwinContext.GetUserManager<UserService>();
// Find out user
var user = await service.FindAsync(context.UserName, context.Password);
// If the user is not found
if (user == null)
{
// Set an error
context.SetError("invalid_grant", "The user name or password is incorrect.");
// Return from the function
return;
}
// If the user has not confirmed their account
if (!user.EmailConfirmed)
{
// Set an error
context.SetError("invalid_grant", "User did not confirm email.");
// Return from the function
return;
}
// Generate the identity for the user
var oAuthIdentity = await user.GenerateUserIdentityAsync(service, "JWT");
// Create a new ticket
var ticket = new AuthenticationTicket(oAuthIdentity, null);
// Add the ticked to the validated context
context.Validated(ticket);
}
}
which is pretty straight forward. Also, the CustomJwtFormat class looks like this:
/// <summary>
/// JWT Format
/// </summary>
public class CustomJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
// Create our private property
private readonly string issuer;
/// <summary>
/// Default constructor
/// </summary>
/// <param name="issuer">The issuer</param>
public CustomJwtFormat(string issuer)
{
this.issuer = issuer;
}
/// <summary>
/// Method to create our JWT token
/// </summary>
/// <param name="data">The Authentication ticket</param>
/// <returns></returns>
public string Protect(AuthenticationTicket data)
{
// If no data is supplied, throw an exception
if (data == null)
throw new ArgumentNullException("data");
// Get our values from our appSettings
string audienceId = ConfigurationManager.AppSettings["as:AudienceId"];
string symmetricKeyAsBase64 = ConfigurationManager.AppSettings["as:AudienceSecret"];
// Decode our secret and encrypt the bytes
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var signingKey = new HmacSigningCredentials(keyByteArray);
// Get our issue and expire dates in UNIX timestamps
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
// Create our new token
var token = new JwtSecurityToken(this.issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
// Create a handler
var handler = new JwtSecurityTokenHandler();
// Write our token string
var jwt = handler.WriteToken(token);
// Return our token string
return jwt;
}
/// <summary>
///
/// </summary>
/// <param name="protectedText"></param>
/// <returns></returns>
public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
}
This is the crucial bit. It uses Thinktecture to generate the token. The line that looks like this:
// Create our new token
var token = new JwtSecurityToken(this.issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
// Create a handler
var handler = new JwtSecurityTokenHandler();
// Write our token string
var jwt = handler.WriteToken(token);
This returns what you would expect from the token (access_token, expires_in and token_type) but I would like to return some user information too. Things like username, roles, etc.
Anyone know how I can do this?
The username and roles are present in the authenticated identities claims and therefore persisted in the JWT that is sent back in the access token.
So the line:
var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims,issued,expires,signingKey);
Inserts the claims from the auth'd identity:
i.e if I did this in the OAuth provider:
```
IList<Claim> claims = new List<Claim>();
if (context.UserName.Equals("spencer") && context.UserName.Equals(context.Password))
{
claims.Add(new Claim(ClaimTypes.Name, user.DisplayName));
claims.Add(new Claim(ClaimTypes.Role, "User"));
}
));
var claimIdentity = new ClaimsIdentity(claims);
var ticket = new AuthenticationTicket(claimIdentity, null);
//Now authed and claims are in my identity context
context.Validated(ticket);
```
So now when the JWT is generated those claims are in the token.
You can then decorate your Api Controllers with explicit roles which would then query the "Roles" type in the claimset. If the user doesn't have the role in the role claimset than a 401 is issued:
```
[Route]
[Authorize(Roles ="User,Admin")]
public IHttpActionResult Get()
{
return Ok<IEnumerable<Product>>(_products);
}
[Route]
[Authorize(Roles = "Admin")]
public IHttpActionResult Post(Product product)
{
_products.Add(product);
return Created(string.Empty, product);
}
```
So in the above example, if I generate a JWT as me "Spencer" i'm in the users role and the GET would be OK (200) whilst the POST would be Unauthorized (401).
Make sense?
I need to have both windows authentication and owin (forms) authentication but i can't get it to work.
Probably the best option is to have two sites that have different authentication methods.
I found a project that does what i want: MVC5-MixedAuth. But it uses IISExpress and i can't get it to work with Local IIS.
The error that occurs is:
Request filtering is configured on the Web server to deny the request because the query string is too long.
If i remove all my ConfigureAuth() method inside Startup.Auth.cs it doesn't throw the error but i can't login because it is needed to do CookieAuthentication.
Startup.Auth.cs:
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(dbEmployeePortal.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, UserMaster, int>
(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
getUserIdCallback: (id) => (Int32.Parse(id.GetUserId()))
)
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
}
Any idea?
UPDATE 1
The error
Request filtering is configured on the Web server to deny the request because the query string is too long.
appears because occurs a login loop when it tries to reach the login page.
Resolved!
I followed the example: MVC5-MixAuth
Credits: Mohammed Younes
UPDATE 1
Problem: I needed to have both Anonymous Authentication and Windows Authentication enabled.
But when you enable them both, you can only get NT AUTHORITY\IUSR.
Resolution: To get the current user (introduced with NTLM prompt), we need to create an handler that will execute when an user enter at login page.
When the user hits the login page, the handler will get the current windows identity cached in the browser and then set as the LogonUserIdentity.
Note: I needed to use windows first login, when the user hits the login page it will try to get the correspondent ASP.NET User.
Handler
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.AspNet.Identity;
namespace MixedAuth
{
/// <summary>
/// Managed handler for windows authentication.
/// </summary>
public class WindowsLoginHandler : HttpTaskAsyncHandler, System.Web.SessionState.IRequiresSessionState
{
public HttpContext Context { get; set; }
public override async Task ProcessRequestAsync(HttpContext context)
{
this.Context = context;
//if user is already authenticated, LogonUserIdentity will be holding the current application pool identity.
//to overcome this:
//1. save userId to session.
//2. log user off.
//3. request challenge.
//4. log user in.
if (context.User.Identity.IsAuthenticated)
{
this.SaveUserIdToSession(context.User.Identity.GetUserId());
await WinLogoffAsync(context);
context.RequestChallenge();
}
else if (!context.Request.LogonUserIdentity.IsAuthenticated)
{
context.RequestChallenge();
}
else
{
// true: user is trying to link windows login to an existing account
if (this.SessionHasUserId())
{
var userId = this.ReadUserIdFromSession();
this.SaveUserIdToContext(userId);
await WinLinkLoginAsync(context);
}
else // normal login.
await WinLoginAsync(context);
}
}
#region helpers
/// <summary>
/// Executes Windows login action against account controller.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task WinLoginAsync(HttpContext context)
{
var routeData = this.CreateRouteData(Action.Login);
routeData.Values.Add("returnUrl", context.Request["returnUrl"]);
routeData.Values.Add("userName", context.Request.Form["UserName"]);
await ExecuteController(context, routeData);
}
/// <summary>
/// Execute Link Windows login action against account controller.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task WinLinkLoginAsync(HttpContext context)
{
var routeData = this.CreateRouteData(Action.Link);
await ExecuteController(context, routeData);
}
/// <summary>
/// Executes Windows logoff action against controller.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task WinLogoffAsync(HttpContext context)
{
var routeData = this.CreateRouteData(Action.Logoff);
await ExecuteController(context, routeData);
}
/// <summary>
/// Executes controller based on route data.
/// </summary>
/// <param name="context"></param>
/// <param name="routeData"></param>
/// <returns></returns>
private async Task ExecuteController(HttpContext context, RouteData routeData)
{
var wrapper = new HttpContextWrapper(context);
MvcHandler handler = new MvcHandler(new RequestContext(wrapper, routeData));
IHttpAsyncHandler asyncHandler = ((IHttpAsyncHandler)handler);
await Task.Factory.FromAsync(asyncHandler.BeginProcessRequest, asyncHandler.EndProcessRequest, context, null);
}
#endregion
}
}
Extensions
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;
namespace MixedAuth
{
public enum Action { Login, Link, Logoff };
public static class MixedAuthExtensions
{
const string userIdKey = "windows.userId";
//http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
const int fakeStatusCode = 418;
const string controllerName = "Account";
const string loginActionName = "WindowsLogin";
const string linkActionName = "LinkWindowsLogin";
const string logoffActionName = "WindowsLogoff";
const string windowsLoginRouteName = "Windows/Login";
public static void RegisterWindowsAuthentication(this MvcApplication app)
{
app.EndRequest += (object sender, EventArgs e) =>
{
HttpContext.Current.ApplyChallenge();
};
}
/// <summary>
/// Registers ignore route for the managed handler.
/// </summary>
/// <param name="routes"></param>
public static void IgnoreWindowsLoginRoute(this RouteCollection routes)
{
routes.IgnoreRoute(windowsLoginRouteName);
}
/// <summary>
/// By pass all middleware and modules, by setting a fake status code.
/// </summary>
/// <param name="context"></param>
public static void RequestChallenge(this HttpContext context)
{
context.Response.StatusCode = fakeStatusCode;
}
/// <summary>
/// Invoke on end response only. Replaces the current response status code with 401.2
/// </summary>
/// <param name="context"></param>
public static void ApplyChallenge(this HttpContext context)
{
if (context.Response.StatusCode == fakeStatusCode)
{
context.Response.StatusCode = 401;
context.Response.SubStatusCode = 2;
//http://msdn.microsoft.com/en-us/library/system.web.httpresponse.tryskipiiscustomerrors(v=vs.110).aspx
//context.Response.TrySkipIisCustomErrors = true;
}
}
/// <summary>
///
/// </summary>
/// <param name="handler"></param>
/// <param name="action"></param>
/// <returns></returns>
public static RouteData CreateRouteData(this WindowsLoginHandler handler, Action action)
{
RouteData routeData = new RouteData();
routeData.RouteHandler = new MvcRouteHandler();
switch (action)
{
case Action.Login:
routeData.Values.Add("controller", controllerName);
routeData.Values.Add("action", loginActionName);
break;
case Action.Link:
routeData.Values.Add("controller", controllerName);
routeData.Values.Add("action", linkActionName);
break;
case Action.Logoff:
routeData.Values.Add("controller", controllerName);
routeData.Values.Add("action", logoffActionName);
break;
default:
throw new NotSupportedException(string.Format("unknonw action value '{0}'.", action));
}
return routeData;
}
/// <summary>
/// Saves userId to the items collection inside <see cref="HttpContext"/>.
/// </summary>
public static void SaveUserIdToContext(this WindowsLoginHandler handler, string userId)
{
if (handler.Context.Items.Contains(userIdKey))
throw new ApplicationException("Id already exists in context.");
handler.Context.Items.Add("windows.userId", userId);
}
/// <summary>
/// Reads userId from item collection inside <see cref="HttpContext"/>.
/// </summary>
/// <remarks>The item will removed before this method returns</remarks>
/// <param name="context"></param>
/// <returns></returns>
public static int ReadUserId(this HttpContextBase context)
{
if (!context.Items.Contains(userIdKey))
throw new ApplicationException("Id not found in context.");
int userId = Convert.ToInt32(context.Items[userIdKey] as string);
context.Items.Remove(userIdKey);
return userId;
}
/// <summary>
/// Returns true if the session contains an entry for userId.
/// </summary>
public static bool SessionHasUserId(this WindowsLoginHandler handler)
{
return handler.Context.Session[userIdKey] != null;
}
/// <summary>
/// Save a session-state value with the specified userId.
/// </summary>
public static void SaveUserIdToSession(this WindowsLoginHandler handler, string userId)
{
if (handler.SessionHasUserId())
throw new ApplicationException("Id already exists in session.");
handler.Context.Session[userIdKey] = userId;
}
/// <summary>
/// Reads userId value from session-state.
/// </summary>
/// <remarks>The session-state value removed before this method returns.</remarks>
/// <param name="session"></param>
/// <returns></returns>
public static string ReadUserIdFromSession(this WindowsLoginHandler handler)
{
string userId = handler.Context.Session[userIdKey] as string;
if (string.IsNullOrEmpty(userIdKey))
throw new ApplicationException("Id not found in session.");
handler.Context.Session.Remove(userIdKey);
return userId;
}
/// <summary>
/// Creates a form for windows login, simulating external login providers.
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="htmlAttributes"></param>
/// <returns></returns>
public static MvcForm BeginWindowsAuthForm(this HtmlHelper htmlHelper, object htmlAttributes)
{
return htmlHelper.BeginForm("Login", "Windows", FormMethod.Post, htmlAttributes);
}
/// <summary>
/// Creates a form for windows login, simulating external login providers.
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="htmlAttributes"></param>
/// <returns></returns>
public static MvcForm BeginWindowsAuthForm(this HtmlHelper htmlHelper, object routeValues, object htmlAttributes)
{
return htmlHelper.BeginForm("Login", "Windows", FormMethod.Post, htmlAttributes);
}
}
}
Note
You need to have AccountController.cs as partial.
AccountController.Windows.cs
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.AspNet.Identity;
using MixedAuth;
namespace EmployeePortal.Web.Controllers
{
[Authorize]
public partial class AccountController : BaseController
{
//
// POST: /Account/WindowsLogin
[AllowAnonymous]
[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)]
public ActionResult WindowsLogin(string userName, string returnUrl)
{
if (!Request.LogonUserIdentity.IsAuthenticated)
{
return RedirectToAction("Login");
}
var loginInfo = GetWindowsLoginInfo();
// Sign in the user with this external login provider if the user already has a login
var user = UserManager.Find(loginInfo);
if (user != null)
{
SignIn(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
else
{
return RedirectToAction("Login", new RouteValueDictionary(new { controller = "Account", action = "Login", returnUrl = returnUrl }));
}
}
//
// POST: /Account/WindowsLogOff
[HttpPost]
[ValidateAntiForgeryToken]
public void WindowsLogOff()
{
AuthenticationManager.SignOut();
}
//
// POST: /Account/LinkWindowsLogin
[AllowAnonymous]
[HttpPost]
public async Task<ActionResult> LinkWindowsLogin()
{
int userId = HttpContext.ReadUserId();
//didn't get here through handler
if (userId <= 0)
return RedirectToAction("Login");
HttpContext.Items.Remove("windows.userId");
//not authenticated.
var loginInfo = GetWindowsLoginInfo();
if (loginInfo == null)
return RedirectToAction("Manage");
//add linked login
var result = await UserManager.AddLoginAsync(userId, loginInfo);
//sign the user back in.
var user = await UserManager.FindByIdAsync(userId);
if (user != null)
await SignInAsync(user, false);
if (result.Succeeded)
return RedirectToAction("Manage");
return RedirectToAction("Manage", new { Message = ManageMessageId.Error });
}
#region helpers
private UserLoginInfo GetWindowsLoginInfo()
{
if (!Request.LogonUserIdentity.IsAuthenticated)
return null;
return new UserLoginInfo("Windows", Request.LogonUserIdentity.User.ToString());
}
#endregion
}
public class WindowsLoginConfirmationViewModel
{
[Required]
[Display(Name = "User name")]
public string UserName { get; set; }
}
}
Then, you need to add the handler:
<add name="Windows Login Handler" path="Login" verb="GET,POST" type="MixedAuth.WindowsLoginHandler" preCondition="integratedMode" />
Startup.cs
app.CreatePerOwinContext(dbEmployeePortal.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
PathString path = new PathString("/Account/Login");
if (GlobalExtensions.WindowsAuthActive)
path = new PathString("/Windows/Login");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
//LoginPath = new PathString("/Account/Login")
LoginPath = path
});
// Use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
Then you need to configure Local IIS to use WindowsAuthentication and AnonymousAuthentication. You can do this in the Authentication Module.
Note If you don't have WindowsAuthentication go to Control Panel then Programs and Features then "Turn Windows features on or off":
select "Internet Information Services" > "World Wide Web" > "Security"
and select Windows Authentication.
I didn't see this in your answer, so for anyone looking on how to capture Windows Auth in your Owin pipeline, you also can add the following to your ConfigureAuth method:
public void ConfigureAuth(IAppBuilder app)
{
HttpListener listener = (HttpListener)app.Properties["System.Net.HttpListener"];
listener.AuthenticationSchemes = AuthenticationSchemes.IntegratedWindowsAuthentication;
}
I am using the newest version of ASP.NET Identity, using the sample project as a base template.
My changes are simple: Adding a FIRM Properyt to the application user.
Thus, the application User looks like this:
public class ApplicationUser : IdentityUser
{
/// <summary>
/// Gets or sets the firm.
/// </summary>
/// <value>
/// The firm.
/// </value>
public virtual Firm Firm { get; set; }
/// <summary>
/// Generates the user identity asynchronous.
/// </summary>
/// <param name="manager">The manager.</param>
/// <returns>A claims identity</returns>
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
//// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
//// Add custom user claims here
return userIdentity;
}
}
Also, I do have a Unit Of Work implented, which is responsible for Firm and other entities.
/// <summary>
/// This contains a Unit of Work pattern Implementation. Provides access to all Elements used in this Application.
/// </summary>
public class UnitOfWork : IDisposable, IUnitOfWork
{
/// <summary>
/// The database context
/// </summary>
private PriorityDBContext dbContext = new PriorityDBContext();
public UnitOfWork(ApplicationDbContext dbContext)
{
this.dbContext = (PriorityDBContext)ApplicationDbContext;
}
private GenericRepository<Firm> firmRepository;
public IGenericRepository<Firm> FirmRepository
{
get
{
if (this.firmRepository == null)
{
this.firmRepository = new GenericRepository<Firm>(this.dbContext);
}
return this.firmRepository;
}
}
}
Now to my problem: I have an Account Controller, which can create new Users (among other things). When a new User is created, the Firm is silently doubled. E.g. I had a Firm called "Example1" in the database before. Now I create a new User and suddenly the Database contains "Example1" two times.
As far I can tell, the root cause is that Asp.net Identity uses one DbContext, my UnitOfWork another. I am unsure why, I tried to set up Ninject in a way that the same DbContext is used in all parts. But it simply have enormous problems getting the same db Context into the UserManager and the unit of work :(
[Authorize]
public class AccountController : Controller
{
/// <summary>
/// The Unit of Work
/// </summary>
private IUnitOfWork uoW;
/// <summary>
/// The _user manager
/// </summary>
private ApplicationUserManager userManager;
public AccountController(IUnitOfWork uow)
{
this.uoW = uow;
}
/// <summary>
/// Gets the user manager.
/// </summary>
/// <value>
/// The user manager.
/// </value>
public ApplicationUserManager UserManager
{
get
{
if (this.userManager == null)
{
this.userManager = HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
return this.userManager;
}
private set
{
this.userManager = value;
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
bool success = true;
if (ModelState.IsValid)
{
var firm = this.uoW.FirmRepository.GetRaw().Where(f => f.FirmName == newUser.Firm).First();
var user = new ApplicationUser { FirstName = newUser.FirstName, LastName = newUser.LastName, Email = newUser.Email, UserName = newUser.Email, PreferredLanguage = newUser.Language, Firm = firm };
var password = Guid.NewGuid().ToString();
var result = await this.UserManager.CreateAsync(user, password);
if (result.Succeeded)
{
this.logger.Info("Created user for {0} successfully", newUser.Email);
}
}
}
}
}
}
Ninject, registering the Bindings:
private static void RegisterServices(IKernel kernel)
{
/// loading the ApplicationDbContext here because the UserManager uses it from the OwinContext and if the
/// Unit of Work defines it's own DBContext, bad things happen, e.g. duplicated objects when working with users AND our own pocos here.
kernel.Bind<IUnitOfWork>().To<UnitOfWork>().WithConstructorArgument("dbContext", ninjectContext => HttpContext.Current.GetOwinContext().Get<ApplicationDbContext>());
kernel.Bind<IGenericRepository<Firm>>().To<GenericRepository<Firm>>();
//kernel.Bind<ApplicationUserManager>().To(HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>());
}
The ApplicationUserManager:
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
}
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
//// Konfigurieren der Überprüfungslogik für Benutzernamen.
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// snipped
return manager;
}
}