Sessions in dart - http

Usually the dart documentation has a lot of useful examples on almost any topic. Unfortunately I could not find anything on sessions in dart.
Could anyone validate this approach as a correct way to do sessions:
Browser sends GET request to sever.
Server responds with web-client.
Web-client sends user credentials.
a) Server checks credentials and generates session cookie.
b) Server sends session cookie back to client.
Web-client stores cookie for further use.
Web-client sends request for some user specific data, and attaches the cookie for verification.
My special interest lies in points 4, 5 and 6, since the others are well documented. If you could share some code snippets on this points, I would very much appreciate it.
EDIT:
After reading the comment from Günter Zöchbauer below I looked into shelf_auth. I realized that it requires rewriting the server app to use shelf.
So I did that.
The main.dart:
// imports of all necessary libraries
main() {
runServer();
}
/**
* Code to handle Http Requests
*/
runServer() {
var staticHandler = createStaticHandler(r"C:\Users\Lukasz\dart\auctionProject\web", defaultDocument: 'auctionproject.html');
var handler = new Cascade()
.add(staticHandler) // serves web-client
.add(routes.handler) // serves content requested by web-client
.handler;
io.serve(handler, InternetAddress.LOOPBACK_IP_V4, 8080).then((server) {
print('Listening on port 8080');
}).catchError((error) => print(error));
}
The routes.dart
import 'handlers.dart' as handler;
import 'package:shelf_route/shelf_route.dart';
import 'package:shelf_auth/shelf_auth.dart' as sAuth;
Router routes = new Router()
..get('/anonymous', handler.handleAnonymousRequest);
//..post('/login', handler.handleLoginRequest); << this needs to be implemented
//other routs will come later
The handlers.dart
import 'dart:async';
import 'dart:convert';
import 'dart:io' show HttpHeaders;
import 'databaseUtility.dart';
import 'package:shelf_exception_response/exception.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf_path/shelf_path.dart';
shelf.Response handleAnonymousRequest(shelf.Request request) {
return new shelf.Response.ok('got anonymous get request');
}
Unfortunately after reading the shelf_auth documentation I still don't quite know where to add the authentication. They use the Pipline syntax for the handler.

I'll describe how session works in Java with servlets. This could help you in making your implementation work. First off, I have to mention that session and authentication are two separate functions, although the latter depends on the former.
A session helps the server understand consecutive requests coming from the same browser without a big idle time in between. Take a look at the below example:
A user opened a browser A, visited your site
Kept clicking around various links using multiple tabs in browser A
Left the browser idle for 45 minutes
Continued clicking on the pages that he left open
Opened browser B, visited your site
Closed the tab for your website in browser B
Opened another new tab in browser B and clicked on a bookmark to
visit your site
Here is the impact on the server-side session for the above steps of the user:
New session created... let us say JSESSIONID 10203940595940
Same session applies for all requests from all tabs
Session expired on the server, and probably some memory was freed on server
Since Java is not able to locate a session matching JSESSIONID 10203940595940, it creates a new session and asks client to remember new JSESSIONID w349374598457
Requests from new browser are treated as new sessions because the JSESSIONID contract is between a single browser and the server. So, server assigns a new JSESSIONID like 956879874358734
JSESSIONID hangs around in the browser till browser is exited. Closing a tab doesnt clear the JSESSIONID
JSESSIONID is still used by browser, and if not much time elapsed, server would still be hanging on to that session. So, the sesison will continue.
Session use on the server-side:
A session is just a HashMap which maps JSESSIONIDs with another a
bunch of attributes.
There is a thread monitoring elapsed time for the sessions, and
removing JSESSIONIDs and the mapped attributes from memory once a
session expires.
Usually, there is some provision for applications to get an event
alert just when a session becomes ready for expiry.
Implementation details:
User's browser A sends a request to server. Server checks if there is
a Cookie by name JSESSIONID. If none is found, one is created on the
server. The server makes a note of the new JSESSIONID, the created
time, and the last request time which is the same as created time in
this case. In the HTTP response, the server attaches the new
JSESSIONID as a cookie.
Browsers are designed to keep attaching cookies for subsequent visits
to the same site. So, for all subsequent visits to the site, the
browser keeps attaching the JSESSIONID cookie to the HTTP request
header.
So, this time the server sees the JSESSIONID and is able to map the
request to the existing session, in case the session has not yet
expired. In case the session had already expired, the server would
create a new session and attach back the new JSESSIONID as a cookie
in HTTP response.
Authentication mechanisms just make use of the above session handling to detect "new sessions" and divert them to the login page. Also, existing sessions could be used to store attributes such as "auth-status" - "pass" or "fail".

Below is a small example of how this can be achieved (without client).
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf_session/cookies_middleware.dart';
import 'package:shelf_session/session_middleware.dart';
import 'package:shelf_static/shelf_static.dart';
void main(List<String> args) async {
final router = Router();
router.get('/', _handleHome);
router.get('/login', _handleLogin);
router.get('/login/', _handleLogin);
router.post('/login', _handleLogin);
router.post('/login/', _handleLogin);
router.get('/logout', _handleLogout);
router.get('/logout/', _handleLogout);
final staticHandler =
createStaticHandler('../web', defaultDocument: 'index.html');
final handler = Cascade().add(staticHandler).add(router).handler;
final pipeline = const Pipeline()
.addMiddleware(logRequests())
.addMiddleware(cookiesMiddleware())
.addMiddleware(sessionMiddleware())
.addHandler(handler);
const address = 'localhost';
const port = 8080;
final server = await io.serve(pipeline, address, port);
print('Serving at http://${server.address.host}:${server.port}');
}
const _menu = '''
Home<br />
Log in<br />
Log out<br />''';
Future<Response> _handleHome(Request request) async {
final userManager = UserManager();
final user = userManager.getUser(request);
var body = '$_menu{{message}}<br />{{cookies}}';
if (user == null) {
body = body.replaceAll('{{message}}', 'You are not logged in');
} else {
body = body.replaceAll('{{message}}', 'You are logged in as ${user.name}');
}
final cookies = request.getCookies();
body = body.replaceAll('{{cookies}}',
cookies.entries.map((e) => '${e.key}: ${e.value}').join('<br />'));
request.addCookie(Cookie('foo', 'Foo'));
request.addCookie(Cookie('baz', 'Baz'));
return _render(body);
}
Future<Response> _handleLogin(Request request) async {
const html = '''
<form action="" method="post">
<label>Login</label><br />
<input name="login" type="text" /><br />
<label>Password</label><br />
<input name="password" type="password" /><br /><br />
<button>Log in</button>
</form>
''';
if (request.method == 'GET') {
return _render(_menu + html);
}
final body = await request.readAsString();
final queryParameters = Uri(query: body).queryParameters;
final login = queryParameters['login'] ?? ''
..trim();
final password = queryParameters['password'] ?? ''
..trim();
if (login.isEmpty || password.isEmpty) {
return _render(_menu + html);
}
final user = User(login);
final userManager = UserManager();
userManager.setUser(request, user);
return Response.found('/');
}
Future<Response> _handleLogout(Request request) async {
Session.deleteSession(request);
return Response.found('/');
}
Response _render(String body) {
return Response.ok(body, headers: {
'Content-type': 'text/html; charset=UTF-8',
});
}
class User {
final String name;
User(this.name);
}
class UserManager {
User? getUser(Request request) {
final session = Session.getSession(request);
if (session == null) {
return null;
}
final user = session.data['user'];
if (user is User) {
return user;
}
return null;
}
User setUser(Request request, User user) {
var session = Session.getSession(request);
session ??= Session.createSession(request);
session.data['user'] = user;
return user;
}
}
For client-server (password implies password hash):
Server code:
class ClientApi {
final _authenticate = createMiddleware(
requestHandler: (Request request) async {
final headers = request.headers;
final xAuthKey = headers['X-Auth-Key'];
if (xAuthKey is! String) {
return Response(401);
}
final xAuthEmail = headers['X-Auth-Email'];
if (xAuthEmail is! String) {
return Response(401);
}
final connection = await getConnection();
final statement = SelectStatement();
statement.fields.add('id');
statement.fields.add('password');
statement.tables.add('customers');
statement.where.add('id = ?');
final rows = await connection.query('$statement;', [xAuthEmail]);
for (final row in rows) {
final fields = row.fields;
final password = fields['password'] as String;
final apiKey = _getApiKey(password);
if (xAuthKey == apiKey) {
return null;
}
}
return Response(401);
},
);
Handler get handler {
final router = Router();
final routes = {
'login': _login,
};
for (final key in routes.keys) {
final value = routes[key]!;
router.post('/$key', const Pipeline().addHandler(value));
router.post('/$key/', const Pipeline().addHandler(value));
}
final routes2 = {
'add_to_cart': _addToCart,
};
for (final key in routes2.keys) {
final value = routes2[key]!;
router.post('/$key',
const Pipeline().addMiddleware(_authenticate).addHandler(value));
router.post('/$key/',
const Pipeline().addMiddleware(_authenticate).addHandler(value));
}
return router;
}
Future<Response> _login(Request request) async {
final params = await fromJson(request, LoginRequest.fromJson);
final connection = await getConnection();
final name = params.name.toLowerCase();
final statement = SelectStatement();
statement.tables.add('customers');
statement.fields.add('password');
statement.where.add('id = ?');
final rows = await connection.query('$statement;', [name]);
String? password;
for (final row in rows) {
final fields = row.fields;
password = fields['password'] as String;
break;
}
if (password != null && password == params.password) {
final apiKey = _getApiKey(password);
final user = LoginUser(
apiKey: apiKey,
name: name,
);
return toJson(LoginResponse(user: user).toJson());
}
return toJson(LoginResponse(user: null).toJson());
}
}
Client code:
class ClientApi {
void _addAuthHeaders(Map<String, String> headers) {
final user1 = UserService.user.value;
if (user1 != null) {
headers['X-Auth-Key'] = user1.apiKey;
headers['X-Auth-Email'] = user1.name;
}
}
Future<LoginResponse> login({
required String name,
required String password,
}) async {
final request = LoginRequest(
name: name,
password: password,
);
final json = await _post<Map>(
'login',
body: request.toJson(),
);
final response = LoginResponse.fromJson(json);
return response;
}
Future<T> _post<T>(
String path, {
Map<String, String>? headers,
Object? body,
}) async {
final url = Uri.parse('$_host/$path');
headers ??= {};
headers.addAll({
'Content-type': 'application/json',
});
final response = await http.post(
url,
body: jsonEncode(body),
headers: headers,
);
if (response.statusCode != HttpStatus.ok) {
throw StateError('Wrong response status code: ${response.statusCode}');
}
final json = jsonDecode(response.body);
if (json is! T) {
throw StateError('Wrong response');
}
return json;
}
}
Somewhere in the UI component.
void onClick(Event event) {
event.preventDefault();
final bytes = utf8.encode(password);
final digest = sha256.convert(bytes);
Timer.run(() async {
try {
final clientApi = ClientApi();
final response =
await clientApi.login(name: name, password: digest.toString());
final user = response.user;
if (user != null) {
UserService.user.value = User(apiKey: user.apiKey, name: user.name);
}
} catch (e) {
//
}
changeState();
});
}

Related

(Blazor-server-side / SignalR / Net6) HubConnectionContext loses user in HttpContext the second time it's called

I am trying to send a message with SignalR to a specific user.
I implemented the default project authentication with Blazor Server side and Net6.
I can log in / log out / register.
I implemented the IUSerIdProvider Interface to get the UserId.
The first time I launch the app, I can retrieved the user (from connection.GetHttpContext(); or connection.User.FindFirstValue(ClaimTypes.Name); but when I navigate to an other page and call the hub again, the HubConnectionContext loses my User and all his informations.
If I force the id with a constant string it works but why do I lose the informations the second time ?
I don't know if I need to use cookies because the first time I have informations.
// CustomUserIdProvider.cs
public class CustomUserIdProvider : IUserIdProvider
{
public string? GetUserId(HubConnectionContext connection)
{
var httpContext = connection.GetHttpContext();
var userId = connection.User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrWhiteSpace(userId))
return string.Empty;
return userId;
}
}
// Program.cs
-----
builder.Services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
-----
app.UseAuthentication();
app.UseAuthorization();
// SignalR.razor (where I test to receive / send a message and here I lost the informations)
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/notifyhub"))
.Build();
hubConnection.On<int, string>("ReceiveMessage", (id, message) =>
{
var encodedMsg = $"{id}: {message}";
InvokeAsync(StateHasChanged);
});
await hubConnection.StartAsync();
}
private async Task Send()
{
if (hubConnection is not null)
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
authMessage = $"{user.Identity.Name} is authenticated.";
claims = user.Claims;
surnameMessage =
$"Surname: {user.FindFirst(c => c.Type == ClaimTypes.Surname)?.Value}";
await hubConnection.SendAsync("Send", user.Identity.Name, 1, "Message envoyé");
}
}

How can you authenticate requests made to a Discord interactions webhook in .NET?

According to the Discord documentation, webhooks must validate the headers on every request in order to be accepted. The documentation provides the following code sample:
const nacl = require('tweetnacl');
// Your public key can be found on your application in the Developer Portal
const PUBLIC_KEY = 'APPLICATION_PUBLIC_KEY';
const signature = req.get('X-Signature-Ed25519');
const timestamp = req.get('X-Signature-Timestamp');
const body = req.rawBody; // rawBody is expected to be a string, not raw bytes
const isVerified = nacl.sign.detached.verify(
Buffer.from(timestamp + body),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex')
);
if (!isVerified) {
return res.status(401).end('invalid request signature');
}
How do you do this in .NET 5.0? I haven't been able to find any examples of Ed25519 validation.
This implementation requires the NSec.Cryptography NuGet package.
First, you must create an ActionFilter to place on your WebAPI controller or endpoints. The simplest way to do this is by extending ActionFilterAttribute:
public class DiscordAuthorizationActionFilterAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// This is needed to move the request stream to the beginning as it has already been evaluated for model binding
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
var signature = context.HttpContext.Request.Headers["X-Signature-Ed25519"].FirstOrDefault();
var timestamp = context.HttpContext.Request.Headers["X-Signature-Timestamp"].FirstOrDefault();
var body = await new StreamReader(context.HttpContext.Request.Body).ReadToEndAsync();
var key = "{YOUR API KEY HERE}";
var algorithm = SignatureAlgorithm.Ed25519;
var publicKey = PublicKey.Import(algorithm, GetBytesFromHexString(key), KeyBlobFormat.RawPublicKey);
var data = Encoding.UTF8.GetBytes(timestamp + body);
var verified = algorithm.Verify(publicKey, data, GetBytesFromHexString(signature));
if (!verified)
context.Result = new UnauthorizedObjectResult("Invalid request");
else
await next();
}
private byte[] GetBytesFromHexString(string hex)
{
var length = hex.Length;
var bytes = new byte[length / 2];
for (int i = 0; i < length; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
}
Note this comment:
// This is needed to move the request stream to the beginning as it has already been evaluated for model binding
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
To allow the request body stream to be reused, you must explicitly enable this in the request pipeline prior to the first time it is accessed, likely during model binding. To do this, you can add a simple step in Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{
app.UseStaticFiles();
app.UseRouting();
// This is needed to retrieve request body as JSON string in ActionFilter
app.Use(async (context, next) =>
{
var controller = context.Request.RouteValues["controller"] as string;
if (controller == "Discord")
context.Request.EnableBuffering();
await next();
});
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
Notice the check for the controller name; since I placed the attribute on DiscordController, the controller value stored in the RouteValues collection is "Discord".
Finally, simply add the attribute to an endpoint that accepts POST requests:
public class DiscordController : ControllerBase
{
[DiscordAuthorizationActionFilter]
[HttpPost]
public async Task<IActionResult> PostAsync(DiscordInteraction interaction)
{
if (interaction == null)
return BadRequest();
if (interaction.Type == DiscordInteractionType.Ping)
return Ok(new { Type = 1 });
// Request processing here
return Ok();
}
}
Note that the DiscordInteraction model is custom code not available in any libraries that I'm aware of. It is simple to create this by following the documentation. To test this, I found it helpful to use ngrok to route requests from Discord to my development environment.

How to invalidate 'expired' Firebase Instance ID Token

AFAIK, the Firebase Instance Token will be refreshed under the following 4 conditions:
App deletes Instance ID
App is restored on a new device
User uninstalls/reinstall the app
User clears app data
Suppose a user is using Token A as his 'FCM address'. Every time when he logs in the app, he will register the Token A to the Firestore along with this user's UUID so user-specific cloud message can be sent to him. When he logs out, the system will fire a request to firestore for removing the token A record.
Now, when the user reinstalls the app, the instance id is refreshed and a new Token B is generated. The Token A becomes useless. Unfortunately, if the user does not log out before the uninstallation, token A will stay in the firestore forever.
Any workaround or wiser way to handle this case?
Keeping your token registry up to date requires two steps:
Remove outdated tokens from your application code.
Check for outdated tokens and remove them when you send messages.
Your approach of removing a token that is no longer used, is #1.
The second step though is to remove tokens from your registry/database when you get a messaging/invalid-registration-token or messaging/registration-token-not-registered response when trying to send a message to it. The functions-samples repo contains a great example of this:
admin.messaging().sendToDevice(tokens, payload).then((response) => {
// For each message check if there was an error.
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('Failure sending notification to', tokens[index], error);
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
// TODO: remove the token from your registry/database
}
}
});
});
The above code uses the Firebase Admin SDK for Node.js, but the same logic could also be applied to other platforms or when sending messages through the HTTPS endpoints.
As Frank mentioned in his answer you can remove them when sending messages and getting not registered error.
Here how I delete outdated registration tokens when registering a new one using C#.
First of all using Instance ID API I get token info as following:
public async Task<FCMTokenInfo> GetTokenInfoAsync(string token)
{
try
{
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("https://iid.googleapis.com");
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", String.Format("key={0}", "your-authorization-key"));
var uri = $"/iid/info/{token}";
var httpResponse = await client.GetAsync(uri);
var responseStr = await httpResponse.Content.ReadAsStringAsync();
if (httpResponse.StatusCode != HttpStatusCode.OK)
{
//log 400 bad request and do whatever you want
}
var result = JsonConvert.DeserializeObject<FCMTokenInfo>(responseStr);
return result;
}
catch (Exception ex)
{
//log the exception
throw;
}
}
FCMTokenInfo.cs
public class FCMTokenInfo
{
public string Application { get; set; }
public string Subtype { get; set; }
public string Scope { get; set; }
public string AuthorizedEntity { get; set; }
public string Platform { get; set; }
}
And then inside the service that saves registration tokens inside the database:
//this method gets called when a new token is sent by the javascript web app
public async Task AddTokenAsync(Guid accountId, string token)
{
try
{
//getting TokenInfo of the current token(or refreshed one for that app)
var fcmTokenInfo = await firebaseServices.GetTokenInfoAsync(token);
//adding the current token
dbContext.FcmRegisterationTokens.Add(new FcmRegisterationToken
{
Token = token,
AccountId = accountId,
AddingDate = DateTimeOffset.UtcNow,
Application = fcmTokenInfo.Application,
Subtype = fcmTokenInfo.Subtype,
AuthorizedEntity = fcmTokenInfo.AuthorizedEntity,
Scope = fcmTokenInfo.Scope,
Platform = fcmTokenInfo.Platform
});
var outdatedTokens = await dbContext.FcmRegisterationTokens
.Where(x => x.AccountId == accountId
&& x.Application == fcmTokenInfo.Application
&& x.Platform == fcmTokenInfo.Platform
).ToListAsync();
//remove them
dbContext.FcmRegisterationTokens.RemoveRange(outdatedTokens);
dbContext.SaveChanges();
}
catch (Exception)
{
throw;
}
}

Is there any possibility return the access_token with only user object?

I am implementing a functionality, where access_token will be sent via email, in this case I need to generate this token with a logic to authenticate the user when accessing the link passed via email.
public async Task<IActionResult> GetLink ()
{
var user = await userManager.FindByEmailAsync("eduardo#test.com.br"); // is active user created
if (user != null)
{
var ident = await userManager.GetAuthenticationTokenAsync(user, "Test", "access_token");
return Ok(ident);
}
return NoContent();
}
Based on the research expected would be something like this, but this is not done with persisted data and my model is not allowing this, anyone have any idea how to persist? Or even just return the token?
I think it is a bad behavior not is not acceptable, but, my user dont have a password for access in this case, maybe is necessary using the token or another mode to login.
It is a very simple flow, this link would be one for a granted action (it will only have read access, basically), and this link will be sent only to a user via email.
The above problem can be solved as follows:
[HttpGet("get_token")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetUserToken([FromServices] ITokenService TS, [FromServices] IUserClaimsPrincipalFactory<EkyteUser> principalFactory,
[FromServices] IdentityServerOptions options)
{
var Request = new TokenCreationRequest();
var user = await userManager.FindByIdAsync(User.GetSubjectId());
var IdentityPricipal = await principalFactory.CreateAsync(user);
var IdServerPrincipal = IdentityServerPrincipal.Create(user.Id.ToString(), user.UserName);
Request.Subject = IdServerPrincipal;
Request.IncludeAllIdentityClaims = true;
Request.ValidatedRequest = new ValidatedRequest();
Request.ValidatedRequest.Subject = Request.Subject;
Request.ValidatedRequest.SetClient(Config.GetClient());
Request.Resources = new Resources(Config.GetResources(), Config.GetApiResources());
Request.ValidatedRequest.Options = options;
var Token = await TS.CreateAccessTokenAsync(Request);
Token.Issuer = "http://" + HttpContext.Request.Host.Value;
var TokenValue = await TS.CreateSecurityTokenAsync(Token);
return Ok(TokenValue);
}
It is necessary to identify the user, set the necessary resources and consequently the client that is accessing. After that, just include the access host to generate the token.

How to refresh claim of a different user than the one logged in during the current request?

I use the following code to update the claims of a user in my web application. However, to update the claims/cookie of this user, I want to force him to login again. So essentially I want to expire his cookie after I update the claims. Any idea how to do this?
await _signInManager.RefreshSignInAsync(user); is the first thing I tried, but fails because I'm updating the claims of another user (the one that is currently logged in) :)
All other examples I found are more or less the same as RefreshSignInAsync and do not deal with the fact that I'm updating the claims of another user.
public async Task<IActionResult> AddClaimPost(string id)
{
var user = _context.ApplicationUser
.SingleOrDefault(m => m.Id == id);
foreach(var item in Request.Form)
{
if (item.Key.Contains("Claim"))
{
if (item.Value.Contains("true"))
{
if (!User.HasClaim(item.Key, item.Key))
{
var result = await _userManager.AddClaimAsync(user, new Claim(item.Key, item.Key));
}
}
else
{
var result2 = await _userManager.RemoveClaimAsync(user, new Claim(item.Key, item.Key));
}
}
}
await _signInManager.RefreshSignInAsync(user);
return RedirectToAction("Overview");
}
After searching a few days I discovered that what I want is not possible. You cannot force logging the user out without putting the cookie timespan to 0
options.Cookies.ApplicationCookie.ExpireTimeSpan = 0;
In this case it will check the cookie every time the user makes a request. With the following code you can than force the user to login again:
await _userManager.UpdateSecurityStampAsync(user);
I don't recommend the 0 expire timespan approach.
If you have a redis server (or any other persistent data store that is performant) you can do something like:
await redis.StringSetAsync("refresh_login_" + user.Id, "1", null);
Then on every page load you will check this redis value and refresh the signin if the key is set for you:
Filters/BaseActionFilter.cs:
public class BaseActionFilter: IAsyncActionFilter, IAsyncPageFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{ // For classic controllers
await PerformPageTasks(context.HttpContext);
await next();
}
public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context,
PageHandlerExecutionDelegate next)
{ // For razor pages in Areas/
if ((context.HandlerInstance is PageModel page))
{
await PerformPageTasks(context.HttpContext);
}
await next.Invoke();
}
public async Task PerformPageTasks(HttpContext context)
{
var signinManager = context.RequestServices.GetService<SignInManager<MyWebUser>>();
if (signinManager.IsSignedIn(context.User))
{
var cache = context.RequestServices.GetService<IDistributedCache>();
var redis = (await ((RedisCache)cache).GetConnectionAsync()).GetDatabase();
var userManager = context.RequestServices.GetService<UserManager<MyWebUser>>();
var user = await userManager.GetUserAsync(context.User);
if ((await redis.StringGetAsync("refresh_login_" + user.Id)) == "1")
{
await redis.KeyDeleteAsync("refresh_login_" + user.Id);
// refresh the user
await signinManager.RefreshSignInAsync(user);
}
}
}
public async Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
{
await Task.CompletedTask;
}
}
Startup.cs:
services.AddMvc(o =>
{
...
o.Filters.Add(new BaseActionFilter());
}).AddHybridModelBinder();
If you only use traditional controllers or Areas/ razor pages then you can adapt the code accordingly.
Note this requires the user to make an additional page load before the claims are set, so for things like [Authorize] you would need to put this code earlier in the chain and I'm not sure exactly how to do that.

Resources