I'm trying to refresh a session after an ajax request to a controller that has the [AllowAnonymous] attribute. For some reasons removing this attribute is not a possibility right now. The authentication is being made via OWIN (Microsoft.Owin v4.1.0).
Here is how the authentication is made:
public class Startup_Auth
{
public void Configuration(IAppBuilder app)
{
try
{
MyAuthenticationProvider provider = new MyAuthenticationProvider() { OnValidateIdentity = MyValidation };
app.SetDefaultSignInAsAuthenticationType("ExternalCookie");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ExternalCookie",
AuthenticationMode = AuthenticationMode.Active,
CookieName = "MyCookie",
CookieSecure = CookieSecureOption.Always,
LoginPath = new PathString(PATH),
ExpireTimeSpan = TimeSpan.FromMinutes(EXPIRATION),
Provider = provider,
TicketDataFormat = new MyTicketDataFormat()
});
}
...
}
private static Task MyValidation(CookieValidateIdentityContext context)
{
...
}
}
I have also tried by the controller's OnActionExecuting:
[AllowAnonymous]
public class MyController : Controller
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
// can't access cookies here
}
}
Any suggestions will be very welcome.
You need to create a claims identity and call SignIn on the AuthenticationManager (SignInManager.SignInAsync) this way, the Claims are updated:
// Get User and a claims-based identity
ApplicationUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
var Identity = new ClaimsIdentity(User.Identity);
// Remove existing claim and replace with a new value
await UserManager.RemoveClaimAsync(user.Id, Identity.FindFirst("AccountNo"));
await UserManager.AddClaimAsync(user.Id, new Claim("AccountNo", value));
// Re-Signin User to reflect the change in the Identity cookie
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
// [optional] remove claims from claims table dbo.AspNetUserClaims, if not needed
var userClaims = UserManager.GetClaims(user.Id);
if (userClaims.Any())
{
foreach (var item in userClaims)
{
UserManager.RemoveClaim(user.Id, item);
}
}
Related
I want to authorize user in WebApi using ClaimsIdentity. In my AccountController which inherits ApiController class I have my two methods to test user authentication. One is a proper method used to receive user's data from other app based on his AD name and authenticates him saving his data as a Claim. The other one is a test method which I call after the previous one to check if the user is authenticated and has claims set.
Unfortunately the login method doesn't seem to set his Identity correctly even though the cookie is generated. The second method than works as if the user wasn't even authenticated and doesn't have any claims.
I have tried some various combination of creating his Identity but nothing seems to work.
Maybe you can see what I am missing.
AccountController.cs
[HttpGet]
[Route("account/login/{userActDirName}/{realmId}")]
public async Task<IHttpActionResult> Login(string userActDirName, long realmId)
{
//getting user data
var user = await UserManager.FindAsync(userActDirName, "1");
if (user == null)
{
user = new ApplicationUser() { UserName = userActDirName };
IdentityResult result = await UserManager.CreateAsync(user, "1");
if (!result.Succeeded)
{
...
}
user = await UserManager.FindAsync(userActDirName, "1");
}
Authentication.SignOut();
ClaimsIdentity cookieIdentity = UserManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
cookieIdentity.AddClaim(new Claim(ClaimTypes.Name, userActDirName));
cookieIdentity.AddClaim(new Claim("User", JsonConvert.SerializeObject(userData)));
Authentication.SignIn(new AuthenticationProperties() { IsPersistent = false }, cookieIdentity);
}
private ApplicationUserManager _userManager;
private IAuthenticationManager Authentication
{
get { return HttpContext.Current.GetOwinContext().Authentication; }
}
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
IdentityConfig.cs
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>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = false
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = -1,
RequireNonLetterOrDigit = false,
RequireDigit = false,
RequireLowercase = false,
RequireUppercase = false,
};
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
Startup.cs
[assembly: OwinStartup(typeof(Api.Startup))]
namespace Api
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app)
{
System.Web.Helpers.AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
}
Since you used the string "User" while creating claim for complete user object as JSON, using the following code :
cookieIdentity.AddClaim(new Claim("User", JsonConvert.SerializeObject(userData)));
Therefore when checking if the user is authenticated or not, use the follwoing code to check if the above mentioned claim exists or not. It will also give you full JSON that you stored while adding "User" Claim.
Remember the type casting below is very important
also use the following namespace
using System.Security.Claims;
before using the following code
var user = "";
var claims =
((ClaimsIdentity)filterContext.RequestContext.Principal.Identity).Claims;
foreach (var c in claims)
{
if (c.Type == "User")
user = c.Value;
}
I have used this code in a custom "AuthorizationFilterAttribute". Therefore I have
filterContext object
you can get
RequestContext object
easily in any WebAPI-Method e.g.
this.RequestContext.Principal.Identity
therefore,
var claims =
((ClaimsIdentity)this.RequestContext.Principal.Identity).Claims;
will work in any web api controller.
Summary
I have ASP.NET MVC 5 web app with Identity authentication and I have to develop an API with "grant_type" = "authorization_code". This API will be to provide users data to another "well-known" web service that needs a custom error responses. My IDE is Visual Studio Professional 2017. I use Postman to make requests to my Web API.
Documentation I read
In the OWIN and Katana documentation the OWIN OAuth 2.0 Authorization Server link redirects again to main OWIN and Katana page, but I think that I found the source on GitHub: OWIN OAuth 2.0 Authorization Server. I tried to follow this documentation, but there are no examples about this question.
Problem
I can create a new authorization code in my AuthorizationCodeProvider class (with Create() method) when a user authenticates and authorizes the "well-known" web service client to access user's resources. I store this code in a database. When I request a Token AuthorizationCodeProvider.Receive() method is called and the token is deserialized correctly. Then GrantAuthorizationCode() method is called, Postman receives OK response (200 status code) but without token information in body (.AspNet.ApplicationCookie is in cookies).
Detailed explanation and code
This is the Startup class:
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.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, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),
OnApplyRedirect = (context =>
{
// This code is to return custom error response
string path = null;
if (context.Request.Path.HasValue)
path = context.Request.Path.Value;
if (!(path != null && path.Contains("/api"))) // Don't redirect to login page
context.Response.Redirect(context.RedirectUri);
})
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
this.ConfigureAuthorization(app);
}
private void ConfigureAuthorization(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
OAuthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = false,
TokenEndpointPath = new PathString("/api/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new TokenAuthorizationServerProvider(),
AuthorizationCodeProvider = new AuthorizationCodeProvider()
};
app.Use<AuthenticationMiddleware>(); //Customize responses in Token middleware
app.UseOAuthAuthorizationServer(OAuthOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
ConfigureAuthorization() method configures the authorization. It uses classes implemented by me:
AuthenticationMiddleware: the well-known web service wants 401 status responses with custom error JONS instead of the usual 400 status response. It is based on the answer of the question Replace response body using owin middleware.
public class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
var owinResponse = context.Response;
var owinResponseStream = owinResponse.Body;
var responseBuffer = new MemoryStream();
owinResponse.Body = responseBuffer;
await Next.Invoke(context);
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest &&
context.Response.Headers.ContainsKey(BearerConstants.CustomUnauthorizedHeaderKey))
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
string headerValue = context.Response.Headers.Get(BearerConstants.CustomUnauthorizedHeaderKey);
context.Response.Headers.Remove(BearerConstants.CustomUnauthorizedHeaderKey);
ErrorMessage errorMessage = new ErrorMessage(headerValue);
string json = JsonConvert.SerializeObject(errorMessage, Formatting.Indented);
var customResponseBody = new StringContent(json);
var customResponseStream = await customResponseBody.ReadAsStreamAsync();
await customResponseStream.CopyToAsync(owinResponseStream);
owinResponse.ContentType = "application/json";
owinResponse.ContentLength = customResponseStream.Length;
owinResponse.Body = owinResponseStream;
}
}
}
When ErrorMessage is serialized to JSON returns an array of errors:
{
"errors":
[
"message": "the error message"
]
}
I set the BearerConstants.CustomUnauthorizedHeaderKey header in TokenAuthorizationServerProvider.ValidateClientAuthentication() method using a extension method:
public static void Rejected(this OAuthValidateClientAuthenticationContext context, string message)
{
Debug.WriteLine($"\t\t{message}");
context.SetError(message);
context.Response.Headers.Add(BearerConstants.CustomUnauthorizedHeaderKey, new string[] { message });
context.Rejected();
}
This is how TokenAuthorizationServerProvider is implemented:
public class TokenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
// Only for breakpoint. Never stops.
return base.AuthorizeEndpoint(context);
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Check if grant_type is authorization_code
string grantType = context.Parameters[BearerConstants.GrantTypeKey];
if (string.IsNullOrEmpty(grantType) || grantType != BearerConstants.GrantTypeAuthorizationCode)
{
context.Rejected("Invalid grant type"); // Sets header for custom response
return;
}
// Check if client_id and client_secret are in the request
string clientId = context.Parameters[BearerConstants.ClientIdKey];
string clientSecret = context.Parameters[BearerConstants.ClientSecretKey];
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
{
context.Rejected("Client credentials missing"); // Sets header for custom response
return;
}
//Check if client_id and client_secret are valid
ApiClient client = await (new ApiClientService()).ValidateClient(clientId, clientSecret);
if (client != null)
{
// Client has been verified.
Debug.WriteLine($"\t\tClient has been verified");
context.OwinContext.Set<ApiClient>("oauth:client", client);
context.Validated(clientId);
}
else
{
// Client could not be validated.
context.Rejected("Invalid client"); // Sets header for custom response
}
}
public override async Task GrantAuthorizationCode(OAuthGrantAuthorizationCodeContext context)
{
TokenRequestParameters parameters = await context.Request.GetBodyParameters();
using (IUserService userService = new UserService())
{
ApplicationUser user = await userService.ValidateUser(parameters.Code);
if (user == null)
{
context.Rejected("Invalid code");
return;
}
// Initialization.
var claims = new List<Claim>();
// Setting
claims.Add(new Claim(ClaimTypes.Name, user.UserName));
// Setting Claim Identities for OAUTH 2 protocol.
ClaimsIdentity oAuthClaimIdentity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesClaimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);
// Setting user authentication.
IDictionary<string, string> data = new Dictionary<string, string>{ { "userName", user.UserName } };
AuthenticationProperties properties = new AuthenticationProperties(data);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthClaimIdentity, properties);
// Grant access to authorize user.
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesClaimIdentity);
}
}
}
ApiClientService.ValidateClient() checks on database that cliend ID and Secret are correct.
GrantAuthorizationCode() is based on the step 8 from ASP.NET MVC - OAuth 2.0 REST Web API Authorization Using Database First Approach tutorial. But this tutorial for grant_type = password and I think that something is wrong in here.
And the AuthorizationCodeProvider class:
public class AuthorizationCodeProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
AuthenticationTicket ticket = context.Ticket;
string serializedTicket = context.SerializeTicket();
context.SetToken(serializedTicket);
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
// At this point context.Ticket.Identity.IsAuthenticated is true
}
}
I call to create method from the AuthorizationController that shows the Allow/Deny view. It is decorated with System.Web.Mvc.Authorize attribute, so if the user isn't authenticated he or she has to login using the default login page from MVC template project (/account/login):
[Authorize]
public class AuthorizationController : Controller
{
private const string ServiceScope = "service-name";
[HttpGet]
public async Task<ActionResult> Index(string client_id, string response_type, string redirect_uri, string scope, string state)
{
AuthorizationViewModel vm = new AuthorizationViewModel()
{
ClientId = client_id,
RedirectUri = redirect_uri,
Scope = scope,
State = state
};
if (scope == ServiceScope)
{
var authentication = HttpContext.GetOwinContext().Authentication;
authentication.SignIn(
new AuthenticationProperties { IsPersistent = true, RedirectUri = redirect_uri },
new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, User.Identity.Name) },
"Bearer"));
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
[MultiButton(MatchFormKey = "authorization", MatchFormValue = "Allow")]
public async Task<ActionResult> Allow(AuthorizationViewModel vm)
{
if (ModelState.IsValid)
{
string code = await this.SetAuthorizationCode(vm.ClientId, vm.RedirectUri);
if (vm.Scope == ServiceScope)
{
string url = $"{vm.RedirectUri}?code={code}&state={vm.State}";
return Redirect(url);
}
else
{
return Redirect(vm.RedirectUri);
}
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
[MultiButton(MatchFormKey = "authorization", MatchFormValue = "Deny")]
public async Task<ActionResult> Deny(AuthorizationViewModel vm)
{
// Removed for brevity
return View(vm);
}
private async Task<string> SetAuthorizationCode(string clientId, string redirectUri)
{
string userId = User.Identity.GetUserId();
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(clientId, OAuthDefaults.AuthenticationType));
AuthenticationTokenCreateContext authorizeCodeContext = new AuthenticationTokenCreateContext(
HttpContext.GetOwinContext(),
Startup.OAuthOptions.AuthorizationCodeFormat,
new AuthenticationTicket(
identity,
new AuthenticationProperties(new Dictionary<string, string>
{
{ "user_id", userId },
{ "client_id", clientId },
{ "redirect_uri", redirectUri }
})
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.Add(Startup.OAuthOptions.AuthorizationCodeExpireTimeSpan)
}));
Startup.OAuthOptions.AuthorizationCodeProvider.Create(authorizeCodeContext);
string code = authorizeCodeContext.Token;
IUserService userService = new UserService();
await userService.SetAuthorization(userId, true, code); // save to database
userService.Dispose();
return code;
}
}
The authorization code is created in SetAuthorizationCode() method, which is called in Allow() action. This SetAuthorizationCode() method code is based on this answer.
Questions
I now that is very long with a lot of code, but I'm stuck for some days and I didn't find the solution. I don't know the complete flow of the authorization, I think that I'm missing something.
What happens when I call /api/token? I mean, what are the steps in this part of the authentication/authorization flow?
What happens after AuthorizationCodeProvider.GrantAuthorizationCode()?
Why a cookie returned instead of token in the body?
I found the solution of the problem, it was the AuthenticationMiddleware. Once the body of the response is read, it remains empty and does not reach the client. So you have to rewrite the response body.
public class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
var owinResponse = context.Response;
var owinResponseStream = owinResponse.Body;
var responseBuffer = new MemoryStream();
owinResponse.Body = responseBuffer;
await Next.Invoke(context);
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest &&
context.Response.Headers.ContainsKey(BearerConstants.CustomUnauthorizedHeaderKey))
{
// Customize the response
}
else
{
// Set body again with the same content
string body = Encoding.UTF8.GetString(responseBuffer.ToArray());
StringContent customResponseBody = new StringContent(body);
Stream customResponseStream = await customResponseBody.ReadAsStreamAsync();
await customResponseStream.CopyToAsync(owinResponseStream);
}
}
}
What I'm trying to do is accessing user claims which returns from ADFS login. ADFS returns username and with that username I have to run a query to another DB to get user information and store it. I don't really know where to do that and what the best practice is. I can access user claims in the view controller like:
public ActionResult Index()
{
var ctx = Request.GetOwinContext();
ClaimsPrincipal user = ctx.Authentication.User;
IEnumerable<Claim> claims = user.Claims;
return View();
}
But what I need to do is as I said access claims like in global.asax.cs or startup.cs to store user information before the application runs.
This is my Startup.Auth.cs file:
public partial class Startup
{
private static string realm = ConfigurationManager.AppSettings["ida:Wtrealm"];
private static string adfsMetadata = ConfigurationManager.AppSettings["ida:ADFSMetadata"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType
});
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = realm,
MetadataAddress = adfsMetadata
});
}
}
We add an event handler to the WsFederationAuthenticationOptions value in our startup file.
This happens immediately after the security token has been validated.
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions()
{
MetadataAddress = MetadataAddress,
Wtrealm = Wtrealm,
Wreply = CallbackPath,
Notifications = new WsFederationAuthenticationNotifications()
{
SecurityTokenValidated = (ctx) =>
{
ClaimsIdentity identity = ctx.AuthenticationTicket.Identity;
DoSomethingWithLoggedInUser(identity);
}
}
};
I have a custom implementation of the ASP.NET Identity base, using Dapper instead of Entity Framework largely from the tutorial here: http://blog.markjohnson.io/exorcising-entity-framework-from-asp-net-identity/.
Everything is fine with signing users in and out with my AuthenticationManager. However, as soon as I redirect anywhere after logging the user in, the httpcontext is basically null and the user is no longer authenticated. If I use the [Authorize] attribute as well, then the user is automatically declared as Unauthorized, throwing a 401 error.
Here are parts of my AccountController:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(Login login, string redundant)
{
var master = new MasterModel();
if (ModelState.IsValid && (!string.IsNullOrEmpty(login.Email) && !string.IsNullOrEmpty(login.PasswordHash)))
{
var user = await Models.User.FetchUserByEmail(login.Email);
if (user != null)
{
await SignInAsync(user, true);
master.User = user; // User is now signed in - No problem
return RedirectToAction("Overview", "Account", master);
}
}
TempData["Message"] = "Your username or password was not recognised. Please try again.";
return View(master);
}
[HttpGet]
//[Authorize(Roles = "admin,subscriber")] // 403 when uncommented
public ActionResult Overview(MasterModel master = null)
{
// master is just a blank new MasterModel ??
if (!HttpContext.User.Identity.IsAuthenticated)
{
// User is always null/blank identity
TempData["Message"] = "Please log in to view this content";
return RedirectToAction("Login", "Account", master);
}
var userName = string.IsNullOrEmpty(HttpContext.User.Identity.Name)
? TempData["UserName"].ToString()
: HttpContext.User.Identity.Name;
var user = Models.User.FetchUserByEmail(userName).Result;
if (master == null) master = new MasterModel();
master.User = user;
return View(master);
}
My UserStore implements the following interfaces:
public class UserStore : IUserStore<User>, IUserPasswordStore<User>, IUserSecurityStampStore<User>, IQueryableUserStore<User>, IUserRoleStore<User>
My RoleStore just implements IQueryableRoleStore<Role>
User and Role simply implement IUser and IRole respectively
What am I missing?
Update1:
Here's part of the AuthenticatonManager:
public IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
private async Task SignInAsync(User user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
Thanks to #WiktorZychla for pointing out the answer.
Turns out I was missing a fundamental step of adding the cookie authentication to IAppBuilder.
Here's how the OwinStartup.cs now looks for reference:
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
[assembly: OwinStartup(typeof(appNamespace.OwinStartup))]
namespace appNamespace
{
public class OwinStartup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
}
}
}
Hopefully this will save someone else from tearing their hair out!
I have an asp.net mvc application with azure AAD sign in.
When I press f5 to debug the application goes to azure to authenticate in AAD, then it goes back to the application to the controller, and its redirected back again to azure.
I know this because If I put a breakpoint on the Sign In controller it gets hit infinitely
This is my route config
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//routes.IgnoreRoute("");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Dashboards", action = "Dashboard_1", id = UrlParameter.Optional }
);
}
This is my dashboard controller which has authorize
[Authorize]
public class DashboardsController : Controller
{
public ActionResult Dashboard_1()
{
return View();
}
This is my Sign In and sign account controller actions
public class AccountController : Controller
{
public void SignIn()
{
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
public void SignOut()
{
// Remove all cache entries for this user and send an OpenID Connect sign-out request.
string usrObjectId = ClaimsPrincipal.Current.FindFirst(SettingsHelper.ClaimTypeObjectIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(SettingsHelper.AzureADAuthority, new EfAdalTokenCache(usrObjectId));
authContext.TokenCache.Clear();
HttpContext.GetOwinContext().Authentication.SignOut(
OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
}
public ActionResult ConsentApp()
{
string strResource = Request.QueryString["resource"];
string strRedirectController = Request.QueryString["redirect"];
string authorizationRequest = String.Format(
"{0}oauth2/authorize?response_type=code&client_id={1}&resource={2}&redirect_uri={3}",
Uri.EscapeDataString(SettingsHelper.AzureADAuthority),
Uri.EscapeDataString(SettingsHelper.ClientId),
Uri.EscapeDataString(strResource),
Uri.EscapeDataString(String.Format("{0}/{1}", this.Request.Url.GetLeftPart(UriPartial.Authority), strRedirectController))
);
return new RedirectResult(authorizationRequest);
}
public ActionResult AdminConsentApp()
{
string strResource = Request.QueryString["resource"];
string strRedirectController = Request.QueryString["redirect"];
string authorizationRequest = String.Format(
"{0}oauth2/authorize?response_type=code&client_id={1}&resource={2}&redirect_uri={3}&prompt={4}",
Uri.EscapeDataString(SettingsHelper.AzureADAuthority),
Uri.EscapeDataString(SettingsHelper.ClientId),
Uri.EscapeDataString(strResource),
Uri.EscapeDataString(String.Format("{0}/{1}", this.Request.Url.GetLeftPart(UriPartial.Authority), strRedirectController)),
Uri.EscapeDataString("admin_consent")
);
return new RedirectResult(authorizationRequest);
}
public void RefreshSession()
{
string strRedirectController = Request.QueryString["redirect"];
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = String.Format("/{0}", strRedirectController) }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
and this is my startup.auth.cs
public void ConfigureAuth(IAppBuilder app)
{
// configure the authentication type & settings
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// configure the OWIN OpenId Connect options
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = SettingsHelper.ClientId,
Authority = SettingsHelper.AzureADAuthority,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// when an auth code is received...
AuthorizationCodeReceived = (context) => {
// get the OpenID Connect code passed from Azure AD on successful auth
string code = context.Code;
// create the app credentials & get reference to the user
ClientCredential creds = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret);
string userObjectId = context.AuthenticationTicket.Identity.FindFirst(System.IdentityModel.Claims.ClaimTypes.NameIdentifier).Value;
// use the ADAL to obtain access token & refresh token...
// save those in a persistent store...
EfAdalTokenCache sampleCache = new EfAdalTokenCache(userObjectId);
AuthenticationContext authContext = new AuthenticationContext(SettingsHelper.AzureADAuthority, sampleCache);
// obtain access token for the AzureAD graph
Uri redirectUri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
AuthenticationResult authResult = authContext.AcquireTokenByAuthorizationCode(code, redirectUri, creds, SettingsHelper.AzureAdGraphResourceId);
// successful auth
return Task.FromResult(0);
},
AuthenticationFailed = (context) => {
context.HandleResponse();
return Task.FromResult(0);
}
},
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false
}
});
}
We ran into the same issue and solved it by slipping in the Kentor cookie saver. See https://github.com/KentorIT/owin-cookie-saver for details.
To resolve this issue: you can upgrade your application to use ASP.NET Core. If you must continue stay on ASP.NET, perform the following:
Update your application’s Microsoft.Owin.Host.SystemWeb package be at least version and Modify your code to use one of the new cookie manager classes, for example something like the following:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
});
Reference Link