Identity Server 4 - GetProfileDataAsync called multiple times, results in multiple database entries - c#

I have a rather different use case where I have to track the login history, and to track them I'm creating a new GUID in the IProfileService and adding it in the claims and also to the DB , to continue front end task.
The issue is I get multiple DB entry for a single login click, which starts displaying two records per login.
The GUID is used to track the current login session.(We have concurrent login scenario)
Question 1 - Is the GUID creation within this function the right thing, if not where to do it?
Question 2 - Does IDSvr provide a Unique Id for each login, so that i can just use it in the claims.
Question 3- How I stop/minimize the other DB calls made because of multiple calls.
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = DataBaseUserRetrive();
var claims = new List<Claim>
{
//All Claims from user
};
string deviceIdGuid = Guid.NewGuid().ToString();
Claim deviceId = new Claim("device_id", deviceIdGuid ,ClaimValueTypes.String);
claims.Add(deviceId);
await databaseCall(user,deviceIdGuid );
context.IssuedClaims = claims;
}
PS :- I'm quite new to Identity Server

To expand on my comment on the OP - I think a custom implementation of IUserSession (or inheriting and overriding virtual methods in DefaultUserSession) is probably the correct place to intercept session related events and add custom behaviour and logic.
The default implementation is here: https://github.com/IdentityServer/IdentityServer4/blob/3.0.2/src/IdentityServer4/src/Services/Default/DefaultUserSession.cs
Check out the method EnsureSessionIdCookieAsync() - that's probably a good place to wire in any custom logic.
This session ID is what's sent to clients and forms part of the protocol - in particular the front channel logout and session monitoring specs.

Related

Prompt user for additional information during an Open Id Connect event?

Using asp.net Core, Mvc and OpenIdConnect, is it possible to prompt an authenticated user for additional information during the ODIC authentication process, and then redirect back to the originally-desired page?
To give a concrete example: in our system one person, represented by an email address, can have multiple user ids that they may wish to operate under. Assume my email address is tregan#domain.com, and I have 3 user ids to choose from: treganCat, treganDog, treganMouse. When I hit a Controller action that is decorated with the [Authorize] attribute I first go through OpenIdConnect authentication, and one of the claims returned is an email address.
Using that email address, I want the application to prompt me to select the identity that I want to run under (treganDog, treganCat, or treganMouse).
From there, I want the application to take the user id that I selected, interrogate a database for the roles that go along with the selected user id, and load those roles as claims to my identity.
Finally, I want the application to send me on to my desired page (which is the protected Controller method that I originally attempted to visit).
Is this possible?
I'm using an Owin Startup class; the code below "works" except for the fictional line "var identityGuid = [return value from the prompt];" ("fictional" because it represents what I would like to occur, but in fact a series of redirects would be needed).
My example below uses the OnTicketReceived event, but that selection is arbitrary, I would be willing to do this in any event.
services.AddAuthentication(authenticationOptions =>
{
authenticationOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
authenticationOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(openIdConnectOptions =>
{
openIdConnectOptions.Authority = Configuration["PingOne:Authority"];
openIdConnectOptions.CallbackPath = "/Callback";
openIdConnectOptions.ClientId = Configuration["PingOne:ClientId"];
openIdConnectOptions.ClientSecret = Configuration["PingOne:ClientSecret"];
openIdConnectOptions.ResponseType = "code";
openIdConnectOptions.Events.OnTicketReceived = (ticketReceivedContext) =>
{
var emailClaim =
ticketReceivedContext.Principal.Claims.FirstOrDefault(o =>
o.Type == ClaimTypes.Email);
string emailAddress = emailClaim.Value;
//here is where I would like to prompt the user to select an identity based on the email address
//the selected identity is represented by a guid
var identityGuid = [return value from the prompt];
var roles = new MyRepository(myContext).GetRolesForUserId(identityGuid);
var claims = new List<Claim>();
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
ticketReceivedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
return Task.CompletedTask;
};
});
This is impersonation where there is a real user and you need to identify the impersonated user after login.
You will need to complete the login first, return to the app and configure the principal. Then render a UI and receive the selected choice.
You then need your UI to call the back end and tell it to update claims in the auth cookie. Not sure if you'll get this to work though - the impersonated user may need separate storage - such as a second cookie.
This highlights that it can be useful to separate the token / credential the UI receives from the claims the back end works with.
I use the below design a lot for REST APIs that serve UIs directly - though it may be overkill for your solution:
https://authguidance.com/2017/10/03/api-tokens-claims/
I think what I want to do is simply not possible without either figuring out a way to do it inside PingOne or writing my own IdentityServer and taking care of the extra steps there.
I decided to instead write a custom middleware that fires after the Authentication middleware, as described in this SO question: In asp.net core, why is await context.ChallengeAsync() not working as expected?

ASP .NET Core using InMemory Cache per user

I have a system where at some point, the user will be locked to a single page. In this situation his account his locked and he cannot be redirected to any other page and this is after authentication.
The verification is done using Page Filters accessing database. To improve performance I have used memory cache.
However, the result wasn't as expected because once the cache is used for a single user it will affect all the others.
As far as i know, you can separate caching using tag helpers per user but I have no idea if this is possible using code
public async Task<IActionResult> Iniciar(int paragemId, string paragem)
{
var registoId = Convert.ToInt32(User.GetRegistoId());
if (await _paragemService.IsParagemOnGoingAsync(registoId))
{
return new JsonResult(new { started = false, message = "Já existe uma paragem a decorrer..." });
}
else
{
await _paragemService.RegistarInicioParagemAsync(paragemId, paragem, registoId);
_registoService.UpdateParagem(new ProducaoRegisto(registoId)
{
IsParado = true
});
await _registoService.SaveChangesAsync();
_cache.Set(CustomCacheEntries.RecordIsParado, true, DateTimeOffset.Now.AddHours(8));
return new JsonResult(new { started = true, message = "Paragem Iniciada." });
}
}
here i only check first if the user account is blocked in the database first without checking cache first and then create the cache entry.
Every user will be locked because of this.
So my point is... Is there a way to achieve this like tag helpers?
The CacheTagHelper is different than cache in general. It works via the request and therefore can vary on things like headers or cookie values. Just using MemoryCache or IDistributedCache directly is low-level; you're just adding values for keys directly, so there's nothing here to "vary" on.
That said, you can compose your key using something like the authenticated user's id, which would then give each user a unique entry in the cache, i.e. something like:
var cacheKey = $"myawesomecachekey-{User.FindFirstValue(ClaimTypes.NameIdentifier)}";
Short of that, you should use session storage, which is automatically unique to the user, because it's per session.
There are several alternatives to the cache. For details please see this link that describes them in greater detail.
Session State
An alternative would be to store the value in session state. This way, the session of one user does not interfere with the ones of others.
However, there are some downsides of this approach. If the session state is kept in memory, you cannot run your application in a server farm because one server does not know of the others session memory. So you would need to save the session state in a cache (REDIS?) or a database.
In addition, as session memory is stored in the server users cannot change it and avoid the redirection that you try to implement. The downside is that this reduces the amount of users that your server can handle because the server needs to have a specific amount of memory per user.
Cookies
You can send a cookie to the client and check for this cookie when the next request arrives at your server. The downside of this approach is that the user can delete the cookie. If the only consequence of a missing cookie is a request to the database, this is neglectable.
You can use session cookies that are discarded by the server when the session expires.
General
Another hint is that you need to clear the state memory when a user signs out so that with the next sign in, the state is correctly set up for the new user.

How do I use Manatee.Trello with multiple user accounts?

I've been trying the following to retrieve data:
void InitializeTrello()
{
TrelloConfiguration.Serializer = new ManateeSerializer();
TrelloConfiguration.Deserializer = new ManateeSerializer();
TrelloConfiguration.JsonFactory = new ManateeFactory();
TrelloConfiguration.RestClientProvider = new Manatee.Trello.WebApi.WebApiClientProvider();
TrelloConfiguration.ThrowOnTrelloError = true;
}
T DownloadDataFromTrello<T>(TrelloAccount account, Func<T> func)
{
TrelloConfiguration.Cache.Clear();
TrelloAuthorization.Default.AppKey = account.AppKey;
TrelloAuthorization.Default.UserToken = account.UserToken;
T result = func();
TrelloProcessor.Flush();
return result;
}
Method DownloadDataFromTrello is being called a few times with different AppKey and UserToken parametres. I receive the same data every call despite calling TrelloConfiguration.Cache.Clear() inside the function.
I would like to use library without resorting to dirty tricks with unloading static classes and retain the lazy loading functionality. Does anyone know how to use this library with multiple user accounts properly?
All of the entity constructors take a second parameter: a TrelloAuthorization that defaults to TrelloAuthorization.Default. The entity instance uses this authorization throughout its lifetime.
var customAuth = new TrelloAuthorization
{
AppKey = "your app key",
UserToken = "a user's token"
}
var card = new Card("card id", customAuth);
The default cache only looks at the entity ID as the key so even if you change the default authorization you would get the same instances back (using the old auth) if the system is pulling them from a cache (e.g. a card is downloaded as part of a List.Cards enumeration). If you explicitly create the entity through a constructor (as above) the new entity is added to the cache, but only the first one will be returned since it's matched only on ID.
To consider the auth as a match for the key, I'd have to either update the default cache or expose the auth so that you can write your own cache and set the TrelloConfiguration.Cache property. I'm not sure which I prefer right now.
Using a custom auth (possibly in combination with periodically clearing the cache) is currently your best option. Please feel free to create an issue or let me know here if this is a feature you'd like.

Force ASP.Net claims validation after they have been changed

I'm developing an ASP.Net application with OWIN and currently I have a problem with claims. I have two levels of application: basic and advanced. Some features are available for advanced users only. So I check claims, and if user doesn't have claim advanced I return 403. But here I found the workaround which ruins this system:
User activates advanced mode
He performs any action and save its access token
He disactivates advanced mode
Now he's able to perform actions just like he is in advanced mode with this token, however he actually has not permissions to do it.
I'm trying to find some fine solution for this situation but I have no ideas except set 1 minute timeout or always check AspNetUserClaims instead of cookie and so on, but they don't work in my case because he can activate a lifetime feature in this one minute interval and then use it forever.
But i'd like to set some server-side flag like oops, this guy have just changed his cookies, check it from database or something to lower database roundtrips for common API calls.
Is there any standard default way to do it? Or maybe I have just chosen a wrong instrument?
You Need to send update cookies according to your claim value.
Below is code to update your claim value.
Inside your action when user disable/enable advanced mode, Then update user claims.
var isAdvanced= "1";
var identity = (ClaimsIdentity)User.Identity;
// check if claim exist or not.
var existingClaim = identity.FindFirst("IsAdvanced");
if (existingClaim != null)
identity.RemoveClaim(existingClaim);
// add/update claim value.
identity.AddClaim(new Claim("IsAdvanced", isAdvanced));
IOwinContext context = Request.GetOwinContext();
var authenticationContext = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie);
if (authenticationContext != null)
{
authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(identity,authenticationContext.Properties);
}
As soon as you will made a redirection, you will get your get updated claim value, hence you don't need to make database round trip.
Credit to this post.
Unfortunly, the only way I found is actually query DB itself and check if user has valid credentials:
public bool HasRequiredClaims(string[] requiredClaims)
{
using (var context = new ApplicationDbContext())
{
int actualNumberOfClaims = context.Users
.SelectMany(x => x.Claims)
.Count(c => requiredClaims.Contains(c.ClaimValue)); // claim values are unique per user (in my case) so I don't have to filter on user
return actualNumberOfClaims == claimsValuesToSearch.Length;
}
}

Clarifications and peer review regarding authentication and roles of my web application

I am trying to learn basic security and access limitations on ASP MVC.
So far, i have read/watched tutorials but all of them seems different from one another. If i will search something, it will lead me to another implementation which is totally different from what i have.
I implemented Authentication and custom role provider and i have some questions regarding how things work. Majority of explanations that i found from the internet seems overly complicated or outdated.
This is how i implemented my authentication.
login controller:
[HttpGet]
[ActionName("login")]
public ActionResult login_load()
{
return View();
}
[HttpPost]
[ActionName("login")]
public ActionResult login_post(string uname,string pword)
{
using (EmployeeContext emp = new EmployeeContext())
{
int success = emp.login.Where(x => x.username == uname && x.password == pword).Count();
if (success == 1)
{
FormsAuthentication.SetAuthCookie(uname, false);
return RedirectToAction("Details", "Enrollment");
}
return View();
}
}
Then i protected most of my controllers with [Authorize]
Question #1
What's the purpose of FormsAuthentication.SetAuthCookie(uname, false); and what should i typicalfly use it for? would it be alright to store the username. Do i need it for comparison later on?(further security?). It says here that Authentication ticket will be given to the username. Are those the ones with random letters?
--
After that, i decided to dive deeper and implemented a custom role provider
from roleprovider.cs(I only implemented 2 methods so far)
public override string[] GetRolesForUser(string username)
{
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
return null;
}
var cacheKey = username;
if (HttpRuntime.Cache[cacheKey] != null)
{
return (string[])HttpRuntime.Cache[cacheKey];
}
string[] roles = new string[] { };
using (MvcApplication6.Models.EmployeeContext emp = new MvcApplication6.Models.EmployeeContext())
{
roles = (from a in emp.login
join b in emp.roles on a.role equals b.id
where a.username.Equals(username)
select b.role).ToArray<string>();
if (roles.Count() > 0)
{
HttpRuntime.Cache.Insert(cacheKey, roles, null, DateTime.Now.AddMinutes(_cacheTimeoutInMinute), Cache.NoSlidingExpiration);
}
}
return roles;
}
Question #2
I am kinda confused here and i need a deep clarification: so what is basically the purpose of the cacheKey and from my example, i just made it equal to uname since i have no idea what's going on.
Question #3
Why is it returned (string[])HttpRuntime.Cache[cacheKey]; if the value is null? when is it returned and who is receiving it?
Question #4
After getting the value the list of roles from the database, this function will be called HttpRuntime.Cache.Insert(cacheKey, roles, null, DateTime.Now.AddMinutes(_cacheTimeoutInMinute), Cache.NoSlidingExpiration);. So from what i see, the roles are being inserted into the cache? is it for checking the login type later on?
Question #5
from this lines of code:
public override bool IsUserInRole(string uname, string roleName)
{
var userRoles = GetRolesForUser(uname);
return userRoles.Contains(roleName);
}
When are they exactly triggered and who provides the parameters? is the roleName from the cache?
I am having a hard time visualizing what's happening under the hood. Explanations/Referrals will be very helpful.
What's the purpose of FormsAuthentication.SetAuthCookie()?
This is ASP.NET FormsAuthentication's built-in method for dealing with authentication cookies.
How does cookie based authentication work?
Explained: Forms Authentication in ASP.NET 2.0
Basically, it's doing the hard work for you; creating a cookie for a specific user, giving it to them and then using it to recognise the same user in the future. You want to use this function to log a user in (if they enter correct credentials).
The string parameter is for a username. Yes, you can use username.
The bool parameter is for if you want the cookie to be persistent. That is, keep them logged in even if they close the browser (whether or not to use a session).
By using FormsAuthentication in this way, ASP.NET will automatically detect the user again when they visit subsequent pages.
What is basically the purpose of the cacheKey?
The Cache component of the HttpRuntime is for managing a "box" of objects that you might retrieve frequently but don't want to be hitting the database all the time for.
The Cache is implemented as a kind of Key-Value Pair. The cacheKey in your example is a key in the Key-Value collection. You can think of it like other similar data structures used in other languages.
{
"carlobrew": {
"roles": {
"Name": "Administrator"
}
}
}
So you're basically "saving" the roles of the user carlobrew in a container so that you can get them again later. The key in a Key-Value Pair is used to refer back to the data that you put in there. The key you are using to refer back to the saved information is the uname; that is, the username.
The key in Key-Value Pairs is unique, so you cannot have two keys called carlobrew.
Why is it returned (string[])HttpRuntime.Cache[cacheKey]; if the value is null?
There are two steps to using a typical "cache box" like this.
If we find the key (such as the user carlobrew) then we can simply return the data straight away. It's not if the value is null. It's if the value is not null. That's why the code is if (HttpRuntime.Cache[cacheKey] != null).
If the key cannot be found (that is, we don't have the key for carlobrew), well then we have to add it ourselves, and then return it.
Since it's a cache, ASP.NET MVC will automatically delete things from the cache when the timer expires. That's why you need to check to see if the data is null, and re-create it if it is.
The "who is receiving it" is whichever object is responsible for calling the GetRolesForUser() method in the first place.
So from what i see, the roles are being inserted into the cache?
Yes.
Basically, if the data isn't in the cache, we need to grab it from the database and put it in there ourselves, so we can easily get it back if we call the same method soon.
Let's break it down. We have:
Insert(cacheKey, roles, null, DateTime.Now.AddMinutes(_cacheTimeoutInMinute), Cache.NoSlidingExpiration);
Insert is the method. We're calling this.
cacheKey is the key part of the Key-Value Pair. The username.
roles is the object that we want to store in cache. The object can be anything we want.
DateTime.Now.AddMinutes(_cacheTimeoutInMinute) is telling ASP.NET MVC when we want this data to expire. It can be any amount of time that we want. I'm not sure what the variable _cacheTimeoutInMinute maybe it's 5 or 15 minutes.
Cache.NoSlidingExpiration is a special flag. We're telling ASP.NET that, when we access this data, don't reset the expiration timer back to its full. For example, if our timer was 15 mins, and the timer was about to expire with 1 minute to go, if we were using a sliding expiration and tried to access the data, the timer would reset back to 15 minutes and not expire the data.
Not sure what you mean by "is it for checking the login type later on". But no, there isn't any checking of login type here.
IsUserInRole
You would probably call this when the user is trying to do something. For example, if the user goes to /Admin/Index page, then you could check to see if the user is in the Administrator role. If they aren't, you'd return a 401 Unauthorized response and tell you the user they aren't allowed to access that page.
public Controller Admin
{
public ActionResult Index()
{
if (!IsUserInRole("Administrator"))
{
// redirect "not allowed"
}
return View();
}
}

Categories