Can I perform an atomic try-get-or-add operation with a ConcurrentDictionary? - asp.net-core

In my ASP.NET Core MVC web-application (that uses OIDC) I have a class that automatically refreshes the access_token stored in the visitor's authentication cookie before it expires.
It is based on AutomaticTokenManagementCookieEvents from the IdentityServer4 samples. Available here: https://github.com/IdentityServer/IdentityServer4/blob/0155beb2cea850144b6407684a2eda22e4eea3db/samples/Clients/src/MvcHybridAutomaticRefresh/AutomaticTokenManagement/AutomaticTokenManagementCookieEvents.cs
static readonly ConcurrentDictionary<String,Object> _pendingRefreshes = new ConcurrentDictionary<String,Object>();
public override async Task ValidatePrincipal( CookieValidatePrincipalContext context )
{
DateTime accessTokenExpiresAt = GetAccessTokenExpiry( context ); // gets the 'expires_at' value from `context.Properties.GetTokens();`
String refreshToken = GetRefreshToken( context ); // Gets the 'refresh_token' value from `context.Properties.GetTokens();`
Boolean isExpired = DateTime.UtcNow > accessTokenExpiresAt;
Boolean willExpireSoon = DateTime.UtcNow > accessTokenExpiresAt.Subtract( TimeSpan.FromSeconds( 60 ) );
if( isExpired || willExpireSoon )
{
Boolean canRefresh = _pendingRefreshes.TryAdd( refreshToken, null );
if( canRefresh )
{
try
{
await RefreshAccessTokenAsync( context, refreshToken );
}
finally
{
_pendingRefreshes.TryRemove( refreshToken );
}
}
else
{
// TODO: What should happen here?
}
}
}
private async Task RefreshAccessTokenAsync( CookieValidatePrincipalContext context, String refreshToken )
{
// using IdentityModel.Client.HttpClientTokenRequestExtensions.RequestRefreshTokenAsync
TokenResponse response = await this.httpClient.RefreshTokenAsync( refreshToken );
if( response.IsError )
{
// (Error logging code here)
if( response.Error == "invalid_grant" )
{
// Usually invalid_grant errors happen if the user's refresh_token has been revoked or expired
// refresh_token expiry is separate from access_token expiry.
// If a refresh_token has expired or been revoked the only thing to do is force the user to login again. `RejectPrincipal()` will send the user to the OIDC OP login page - though this will cause the user to lose their data if this is a POST request.
context.RejectPrincipal();
}
else
{
// Something else bad happened. Don't invalidate the user's credentials unless they're actually expired, though.
throw new Exception( "Unexpected error." );
}
}
else
{
context.Properties.UpdateTokenValue( "access_token" , response.AccessToken );
context.Properties.UpdateTokenValue( "refresh_token", response.RefreshToken );
DateTime newExpiresAt = DateTime.UtcNow + TimeSpan.FromSeconds( response.ExpiresIn );
context.Properties.UpdateTokenValue( "expires_at", newExpiresAt.ToString( "o", CultureInfo.InvariantCulture ) );
await context.HttpContext.SignInAsync( context.Principal, context.Properties );
}
}
The problem with this code is that if the user's browser makes two simultaneous requests after their access_token has already expired then the user will get an error message if code later-on in the ASP.NET Core pipeline for the second simultaneous request uses the now-expired access_token.
...how can I get it so that the second concurrent request using an expired access_token will await the same Task (from RefreshAccessTokenAsync)?
My thought is something like this:
Change _pendingRefreshes to ConcurrentDictionary<String,Task<String>>.
Change Boolean canRefresh = _pendingRefreshes.TryAdd( refreshToken, null ); to something like this (using a hypothetical TryGetOrAdd method):
Boolean addedNewTask = _pendingRefreshes
.TryGetOrAdd(
key: refreshToken,
valueFactory: rt => this.RefreshTokenAsync( context, rt ),
value: out Task task
);
if( addedNewTask )
{
// wait for the new access_token to be saved before continuing.
await task;
}
else
{
if( isExpired )
{
// If the current request's access_token is already expired and its refresh_token is currently being refrehsed, then wait for it to finish as well, then update the access_token but only for this request's lifetime (i.e. don't call `ReplacePrincipal` or `SignInAsync`.
await task;
}
}
The problem is ConcurrentDictionary<TKey,TValue> does not have a TryGetOrAdd method I can use to atomically get-existing-or-add-new items.
AddOrUpdate - Doesn't return any existing items. Doesn't indicate if the value returned was an existing item or not.
GetOrAdd - Doesn't indicate if the value returned was an existing item or not.
TryAdd - Doesn't let you atomically get any existing value with the same key.
TryGetValue - Doesn't let you atomically add a new item if there isn't a value for the given key.
TryRemove - Doesn't let you atomically add a new item.
TryUpdate - Doesn't let you add new items.
This is fixable using a lock but that negates the advantages of using ConcurrentDictionary. Something like this:
Task<String> task;
Boolean addedNewTask;
lock( _pendingRefreshes )
{
Boolean taskExists = _pendingRefreshes.TryGetValue( refreshToken, out task );
if( taskExists )
{
addedNewTask = false;
}
else
{
task = RefreshAccessTokenAsync( context, refreshToken );
if( !_pendingRefreshes.TryAdd( refreshToken, task ) )
{
throw new InvalidOperationException( "Could not add the Task." ); // This should never happen.
}
addedNewTask = true;
}
}
if( addedNewTask || isExpired )
{
String newAccessToken = await task;
if( isExpired )
{
context.Properties.UpdateTokenValue( "access_token", newAccessToken );
}
}
...or is this the correct use of ConcurrentDictionary for this scenario?

I learned that ConcurrentDictionary does not guarantee that valueFactory won't be invoked exactly once per key, but may be invoked multiple times - which I can't live with because my valueFactory is an expensive operation with side-effects that may adversely negatively affect the user-experience (e.g. invalidating a token stored inside the user's cookies unnecessarily).
I settled on an approach using lock and a Dictionary<String,Task<TokenResponse>> instead. I do wonder how it could be improved and I'd really appreciate any feedback:
private static readonly Dictionary<String,Task<TokenResponse>> _currentlyRunningRefreshOperations = new Dictionary<String,Task<TokenResponse>>();
/// <summary>Returns <c>true</c> if a new Task was returned. Returns <c>false</c> if an existing Task was returned.</summary>
private Boolean GetOrCreateRefreshTokenTaskAsync( CookieValidatePrincipalContext context, String refreshToken, out Task<TokenResponse> task )
{
lock( _currentlyRunningRefreshOperations )
{
if( _currentlyRunningRefreshOperations.TryGetValue( refreshToken, out task ) )
{
return false;
}
else
{
task = this.AttemptRefreshTokensAsync( context, refreshToken );
_currentlyRunningRefreshOperations.Add( refreshToken, task );
return true;
}
}
}
private async Task<TokenResponse> AttemptRefreshTokensAsync( CookieValidatePrincipalContext context, String refreshToken )
{
try
{
TokenResponse response = await this.service.RefreshTokenAsync( refreshToken );
if( response.IsError )
{
this.logger.LogWarning( "Encountered " + nameof(this.service.RefreshTokenAsync) + " error: {error}. Type: {errorType}. Description: {errorDesc}. refresh_token: {refreshToken}.", response.Error, response.ErrorType, response.ErrorDescription, refreshToken );
return response;
}
}
finally
{
lock( _currentlyRunningRefreshOperations )
{
_currentlyRunningRefreshOperations.Remove( refreshToken );
}
}
}

Related

Force refresh IMemoryCache

I am trying to refresh IMemoryCache programmatically. After researching a few links
about Eviction Calback and Clearing cache, I thought I could combine the strategies i.e. clear the cache which would cause the eviction callback to fire. However apparently the post eviction callback won't trigger when the cache is cleared using reflection because it seems the whole cache item with its options (that includes the callback ) is gone. (cache item count goes to 0)
So my question is about refreshing a cache item before expiration, as this issue is still open
private static Dictionary<string, CancellationTokenSource> tokenDict = new Dictionary<string, CancellationTokenSource>();
private MemoryCacheEntryOptions CacheOptions
{
get
{
var expirationToken = new CancellationChangeToken( new CancellationTokenSource(TimeSpan.FromMinutes(ExpirationMinutes + .01)).Token);
var options = new MemoryCacheEntryOptions()
// Do not remove due to memory pressure
.SetPriority(Microsoft.Extensions.Caching.Memory.CacheItemPriority.NeverRemove)
.SetSlidingExpiration(TimeSpan.FromMinutes(ExpirationMinutes))
// Force eviction to run AT expriry, default eviction happens when item is requested after expiry
.AddExpirationToken(expirationToken)
.RegisterPostEvictionCallback(callback: CacheItemRemoved, state: this);
tokenDict[cacheKey] = cancellationTokenSource;
return options;
}
}
private void CacheItemRemoved(object key, object value, EvictionReason reason, object state)
{
_logger.LogInformation($"Reloading {key} cache upon eviction");
switch (key)
{
case AccountCacheKey:
GetAccountCacheAsync();
break;
case FundCacheKey:
GetFundCacheAsync();
break;
default:
break;
}
}
private async Task<List<Account>> GetAccountCacheAsync()
{
return await _cache.GetOrCreateAsync(AccountCacheKey, async entry =>
{
entry.SetOptions(CacheOptions);
var accounts = await LoadAccountsAsync().ConfigureAwait(false);
return accounts;
}).ConfigureAwait(false);
}
private async Task<List<Fund>> GetFundCacheAsync()
{
return await _cache.GetOrCreateAsync(FundCacheKey, async entry =>
{
entry.SetOptions(CacheOptions);
var funds = await LoadFundsAsync().ConfigureAwait(false);
return funds;
}).ConfigureAwait(false);
}
public async Task RefreshCacheAsync()
{
var cacheKeys = new List<string> { AccountCacheKey, FundCacheKey };
foreach (var key in cacheKeys)
{
if (tokenDict.TryGetValue(key, out var token))
{
if (token != null && !token.IsCancellationRequested && token.Token.CanBeCanceled)
{
token.Cancel();
token.Dispose();
}
}
}
}
You already posted a link with the best approach, but you seem to have chosen to go with one of the lower rated answers, which actually doesn't work for your purposes. Instead, you should follow this answer. It creates a cache "manager" class that among other things employs CancellationTokenSource to handle the eviction. That's actually the same method that was recommended in the Github issue you linked, as well.

.net core - How to return 403 on AuthorizationHandler?

I implemented my custom AuthorizationHandler.
On that i check i the user can resolved and is active.
If the user isn't active then i would like to return an 403 status.
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidUserRequirement requirement)
{
var userId = context.User.FindFirstValue( ClaimTypes.NameIdentifier );
if (userId != null)
{
var user = await _userManager.GetUserAsync(userId);
if (user != null)
{
_httpContextAccessor.HttpContext.AddCurrentUser(user);
if (user.Active)
{
context.Succeed(requirement);
return;
}
else
{
_log.LogWarning(string.Format("User ´{1}´ with id: ´{0} isn't active", userId, user.UserName), null);
}
}
else
{
_log.LogWarning(string.Format("Can't find user with id: ´{0}´", userId), null);
}
} else
{
_log.LogWarning(string.Format("Can't get user id from token"), null);
}
context.Fail();
var response = _httpContextAccessor.HttpContext.Response;
response.StatusCode = 403;
}
But i receive a 401. Can you please help me?
Could you check that on the end of your function? I'm using that in my custom middleware to rewrite status code to 401 in some cases but in your scenario should also work
var filterContext = context.Resource as AuthorizationFilterContext;
var response = filterContext?.HttpContext.Response;
response?.OnStarting(async () =>
{
filterContext.HttpContext.Response.StatusCode = 403;
//await response.Body.WriteAsync(message, 0, message.Length); only when you want to pass a message
});
According to the Single Responsibility Principle , we should not use the HandleRequirementAsync() method to redirect reponse , we should use middleware or Controller to do that instead . If you put the redirect logic in HandleRequirementAsync() , how about if you want to use it in View page ?
You can remove the redirection-related code to somewhere else (outside) , and now you inject an IAuthorizationService to authorize anything as you like , even a resource-based authorization :
public class YourController : Controller{
private readonly IAuthorizationService _authorizationService;
public YourController(IAuthorizationService authorizationService)
{
this._authorizationService = authorizationService;
}
[Authorize("YYY")]
public async Task<IActionResult> Index()
{
var resource /* = ... */ ;
var x = await this._authorizationService.AuthorizeAsync(User,resource , "UserNameActiveCheck");
if (x.Succeeded)
{
return View();
}
else {
return new StatusCodeResult(403);
}
}
}
in .NET core 6.0 you can use the Fail method
AuthorizationHandlerContext.Fail Method
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AppAuthorizationRequirement requirement)
{
context.Fail(); //Use this
}

Service Stack - Custom authentication on one route

In my current application, I am using Service Stack with JWT's for security. Security has been implemented and works perfectly. Trouble is, I would like to secure one route differently from the others. There is a document the logged in user retrieves, I want to make sure the document they are retrieving is theirs and not someone else's. It is very sensitive data. I would like to secure it differently because something like PostMan could be used with a valid token to retrieve any document, I want to prevent this. The users id is in the token, I would like to match it against the document that is being retrieved if possible. The current security is implemented like so:
public class AppHost: AppHostBase
{
public override void Configure(Funq.Container container)
{
Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new JsonWebTokenAuthProvider("myKey", "myAudience"),
}));
}
}
JsonWebTokenAuthProvider is a custom class where security was implemented, this all works perfectly. Here is the code:
public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request)
{
// first validate the token, then get roles from session
string header = request.oauth_token;
// if no auth header, 401
if (string.IsNullOrEmpty(header))
{
throw HttpError.Unauthorized(MissingAuthHeader);
}
string[] headerData = header.Split(' ');
// if header is missing bearer portion, 401
if (!string.Equals(headerData[0], "BEARER", StringComparison.OrdinalIgnoreCase))
{
throw HttpError.Unauthorized(InvalidAuthHeader);
}
// swap - and _ with their Base64 string equivalents
string secret = SymmetricKey.Replace('-', '+').Replace('_', '/');
string token = headerData[1].Replace("\"", "");
// set current principal to the validated token principal
Thread.CurrentPrincipal = JsonWebToken.ValidateToken(token, secret, Audience, true, Issuer);
string lanId = GetLanID(Thread.CurrentPrincipal.Identity.Name);
string proxyAsLanId = request.Meta.ContainsKey(META_PROXYID) ? request.Meta[META_PROXYID] : null;
if (HttpContext.Current != null)
{
// set the current request's user the the decoded principal
HttpContext.Current.User = Thread.CurrentPrincipal;
}
// set the session's username to the logged in user
session.UserName = Thread.CurrentPrincipal.Identity.Name;
session.Roles = GetApplicableRoles(lanId, proxyAsLanId);
authService.Request.SetItem("lanID", lanId);
authService.Request.SetItem("proxyAsLanId", proxyAsLanId);
return OnAuthenticated(authService, session, null, null);
}
I looked up RequestFilterAttribute found here, but I do not think that is what I want. Ideally, if the check fails I would like to return a 401 (unauthorized) if possible.
What is the best way to do this?
If you just want to handle one route differently than you can just add the validation in your single Service, e.g:
public object Any(MyRequest dto)
{
var lanId = base.Request.GetItem("lanId");
if (!MyIsValid(lanId))
throw HttpError.Unauthorized("Custom Auth Validation failed");
}
You could do the same in a RequestFilter, e.g:
public class CustomAuthValidationAttribute : RequestFilterAttribute
{
public override void Execute(IRequest req, IResponse res, object responseDto)
{
var lanId = req.GetItem("lanId");
if (!MyIsValid(lanId))
{
res.StatusCode = (int) HttpStatusCode.Unauthorized;
res.StatusDescription = "Custom Auth Validation failed";
res.EndRequest();
}
}
}
And apply it to a single Service:
[CustomAuthValidation]
public object Any(MyRequest dto)
{
//...
}
Or a collection of Services, e.g:
[CustomAuthValidation]
public class MyAuthServices : Service
{
public object Any(MyRequest1 dto)
{
//...
}
public object Any(MyRequest2 dto)
{
//...
}
}

StackExchange.Redis - LockTake / LockRelease Usage

I am using Redis with StackExchange.Redis. I have multiple threads that will at some point access and edit the value of the same key, so I need to synchronize the manipulation of the data.
Looking at the available functions, I see that there are two functions, TakeLock and ReleaseLock. However, these functions take both a key and a value parameter rather than the expected single key to be locked. The intellisene documentation and source on GitHub don't explain how to use the LockTake and LockRelease functions or what to pass in for the key and value parameters.
Q: What is the correct usage of LockTake and LockRelease in StackExchange.Redis?
Pseudocode example of what I'm aiming to do:
//Add Items Before Parallel Execution
redis.StringSet("myJSONKey", myJSON);
//Parallel Execution
Parallel.For(0, 100, i =>
{
//Some work here
//....
//Lock
redis.LockTake("myJSONKey");
//Manipulate
var myJSONObject = redis.StringGet("myJSONKey");
myJSONObject.Total++;
Console.WriteLine(myJSONObject.Total);
redis.StringSet("myJSONKey", myNewJSON);
//Unlock
redis.LockRelease("myJSONKey");
//More work here
//...
});
There are 3 parts to a lock:
the key (the unique name of the lock in the database)
the value (a caller-defined token which can be used both to indicate who "owns" the lock, and to check that releasing and extending the lock is being done correctly)
the duration (a lock intentionally is a finite duration thing)
If no other value comes to mind, a guid might make a suitable "value". We tend to use the machine-name (or a munged version of the machine name if multiple processes could be competing on the same machine).
Also, note that taking a lock is speculative, not blocking. It is entirely possible that you fail to obtain the lock, and hence you may need to test for this and perhaps add some retry logic.
A typical example might be:
RedisValue token = Environment.MachineName;
if(db.LockTake(key, token, duration)) {
try {
// you have the lock do work
} finally {
db.LockRelease(key, token);
}
}
Note that if the work is lengthy (a loop, in particular), you may want to add some occasional LockExtend calls in the middle - again remembering to check for success (in case it timed out).
Note also that all individual redis commands are atomic, so you don't need to worry about two discreet operations competing. For more complexing multi-operation units, transactions and scripting are options.
There is my part of code for lock->get->modify(if required)->unlock actions with comments.
public static T GetCachedAndModifyWithLock<T>(string key, Func<T> retrieveDataFunc, TimeSpan timeExpiration, Func<T, bool> modifyEntityFunc,
TimeSpan? lockTimeout = null, bool isSlidingExpiration=false) where T : class
{
int lockCounter = 0;//for logging in case when too many locks per key
Exception logException = null;
var cache = Connection.GetDatabase();
var lockToken = Guid.NewGuid().ToString(); //unique token for current part of code
var lockName = key + "_lock"; //unique lock name. key-relative.
T tResult = null;
while ( lockCounter < 20)
{
//check for access to cache object, trying to lock it
if (!cache.LockTake(lockName, lockToken, lockTimeout ?? TimeSpan.FromSeconds(10)))
{
lockCounter++;
Thread.Sleep(100); //sleep for 100 milliseconds for next lock try. you can play with that
continue;
}
try
{
RedisValue result = RedisValue.Null;
if (isSlidingExpiration)
{
//in case of sliding expiration - get object with expiry time
var exp = cache.StringGetWithExpiry(key);
//check ttl.
if (exp.Expiry.HasValue && exp.Expiry.Value.TotalSeconds >= 0)
{
//get only if not expired
result = exp.Value;
}
}
else //in absolute expiration case simply get
{
result = cache.StringGet(key);
}
//"REDIS_NULL" is for cases when our retrieveDataFunc function returning null (we cannot store null in redis, but can store pre-defined string :) )
if (result.HasValue && result == "REDIS_NULL") return null;
//in case when cache is epmty
if (!result.HasValue)
{
//retrieving data from caller function (from db from example)
tResult = retrieveDataFunc();
if (tResult != null)
{
//trying to modify that entity. if caller modifyEntityFunc returns true, it means that caller wants to resave modified entity.
if (modifyEntityFunc(tResult))
{
//json serialization
var json = JsonConvert.SerializeObject(tResult);
cache.StringSet(key, json, timeExpiration);
}
}
else
{
//save pre-defined string in case if source-value is null.
cache.StringSet(key, "REDIS_NULL", timeExpiration);
}
}
else
{
//retrieve from cache and serialize to required object
tResult = JsonConvert.DeserializeObject<T>(result);
//trying to modify
if (modifyEntityFunc(tResult))
{
//and save if required
var json = JsonConvert.SerializeObject(tResult);
cache.StringSet(key, json, timeExpiration);
}
}
//refresh exiration in case of sliding expiration flag
if(isSlidingExpiration)
cache.KeyExpire(key, timeExpiration);
}
catch (Exception ex)
{
logException = ex;
}
finally
{
cache.LockRelease(lockName, lockToken);
}
break;
}
if (lockCounter >= 20 || logException!=null)
{
//log it
}
return tResult;
}
and usage :
public class User
{
public int ViewCount { get; set; }
}
var cachedAndModifiedItem = GetCachedAndModifyWithLock<User>(
"MyAwesomeKey", //your redis key
() => // callback to get data from source in case if redis's store is empty
{
//return from db or kind of that
return new User() { ViewCount = 0 };
},
TimeSpan.FromMinutes(10), //object expiration time to pass in Redis
user=> //modify object callback. return true if you need to save it back to redis
{
if (user.ViewCount< 3)
{
user.ViewCount++;
return true; //save it to cache
}
return false; //do not update it in cache
},
TimeSpan.FromSeconds(10), //lock redis timeout. if you will have race condition situation - it will be locked for 10 seconds and wait "get_from_db"/redis read/modify operations done.
true //is expiration should be sliding.
);
That code can be improved (for example, you can add transactions for less count call to cache and etc), but i glad it will be helpfull for you.

How to correct issue with user being redirected to login after successful, but not to user page?

In MVC4, I created a custom membership provider that returns true if the user authentication passes. No biggie here - this portion works the way it should:
public override bool ValidateUser(string username, string password)
{
var crypto = new SimpleCrypto.PBKDF2(); // type of encryption
// TODO: using (var unitOfWork = new Website.Repository.UnitOfWork(_dbContext))
//var unitOfWork1 = new Website.Repository.UnitOfWork(_dbContext);
using (var db = new Website.DAL.WebsiteDbContext())
{
var user = db.Users
.Include("MembershipType")
.FirstOrDefault(u => u.UserName == username);
if (user != null && user.Password == crypto.Compute(password, user.PasswordSalt))
{
FormsAuthentication.SetAuthCookie(username, true);
return true;
}
}
return false;
}
In my Login Action:
[HttpPost]
[AllowAnonymous]
public ActionResult Login(Models.UserModel user)
{
if (ModelState.IsValid)
{
// custom membership provider
if (Membership.ValidateUser(user.UserName, user.Password))
{
// Cannot use this block as user needs to login twice
//if (User.IsInRole("WaitConfirmation")) // checks the custom role provider and caches based on web.config settings
//{
// //TempData["EmailAddress"] = thisUser.Email;
// // email address has not yet been confirmed
// return RedirectToAction("WaitConfirmation");
// //return View("Account", thisUser)
//}
//else
//{
// // get custom identity - user properties
// string userName = UserContext.Identity.Name;
// //CustomIdentity identity = (CustomIdentity)User.Identity;
// var identity = UserContext.Identity;
// int userId = identity.UserId;
// return RedirectToAction("Index", "Dashboard");
//}
if (User.Identity.IsAuthenticated && User.IsInRole("WaitConfirmation")) // checks the custom role provider and caches based on web.config settings
{
return RedirectToAction("WaitConfirmation");
}
else if (User.Identity.IsAuthenticated)
{
// get custom identity - user properties
string userName = UserContext.Identity.Name;
return RedirectToAction("Index", "Dashboard");
}
}
else
{
ModelState.AddModelError("", "Login data is incorrect.");
}
}
return View(user);
}
In stepping through the code, when a user first logs in, User.Identity.IsAuthenticated is false and the page is redirected back to the login page. At this point if I either:
manually navigate to the user page (Dashboard) the user is details are available
login again, this works
I believe the answer lies somewhere in why the User.Identity.IsAuthenticated is not immediately true but can't figure out this is false the first time around.
The first block of commented-out code fails with Unable to cast object of type 'System.Security.Principal.GenericIdentity' to type 'Website.AdminWebsite.Infrastructure.CustomIdentity' as there is no IsAuthenticated check.
Suggestions?
This post describes a problem with similar symptoms.
http://forums.asp.net/t/1177741.aspx
Please have a read and ensure the order of your events (i.e. Authenticate, LoggedIn)
After reading the article #mcsilvio's suggested, I added an RedirectToAction() as follows to initiate a new page life-cycle:
public ActionResult Login(Models.UserModel user)
{
if (ModelState.IsValid)
{
// custom membership provider
if (Membership.ValidateUser(user.UserName, user.Password))
{
return RedirectToAction("VerifyIdentity", user);
}
else
{
ModelState.AddModelError("", "Login data is incorrect.");
}
}
return View(user);
}
public ActionResult VerifyIdentity(Models.UserModel user)
{
if (User.Identity.IsAuthenticated && User.IsInRole("WaitConfirmation")) // checks the custom role provider and caches based on web.config settings
{
return RedirectToAction("WaitConfirmation");
}
else if (User.Identity.IsAuthenticated)
{
// get custom identity - user properties
string userName = UserContext.Identity.Name;
return RedirectToAction("Index", "Dashboard");
}
return View(User);
}
This did the trick, but I'm wondering if there is a better way or is it always done like this?