How State management of claims work in Blazor Server? - c#

I have implemented Identity Server for Auth Code Flow.
What is correct way to persist the claims (in OnTicketReceived or OnTicketValidated as shown below), so that in subsequent calls
to Blazor pages, I could receive User aka ClaimPrincipal populated for
my use?
Here is code of middleware of my resource server:
public void ConfigureServices(IServiceCollection services)
{
//....
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
options.SignIn.RequireConfirmedEmail = false;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<SomeContext>()
.AddDefaultTokenProviders();
Middleware integration:
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
options.DefaultSignOutScheme = "oidc";
})
.AddCookie("cookie", options =>
{
options.Cookie.Name = "__Host-bff";
options.Cookie.SameSite = SameSiteMode.Strict;
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5001";
options.ClientId = "mvc.code";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.ResponseMode = "query";
options.GetClaimsFromUserInfoEndpoint = true;
options.MapInboundClaims = false;
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
//Critical Parts
options.TokenValidationParameters = new()
{
NameClaimType = "name",
RoleClaimType = "role"
};
Authenticated callback from identity server is as following:
options.Events.OnTicketReceived = async n =>
{
var serviceProvider = n.HttpContext.RequestServices;
var accountService = serviceProvider.GetService<IAccountService>() ?? throw new ArgumentNullException("serviceProvider.GetService<IAccountService>()");
I tried using BlazoredSessionStorage etc. but it seems too early to
invoke that. We have to wait until OnPrerender or OnInit
I also tried CustomTokenStore. But how does the claim from cookie
come back to server?
var svc = n.HttpContext.RequestServices.GetRequiredService<IUserAccessTokenStore>();
if (n.Principal != null)
{
var userName = n.Principal.FindFirst(x => x.Type == "name")?.Value;
await accountService.UserCreateAsync(new NewAccount
{
Username = userName,
FirstName = userFirstName,
LastName = userLastName,
//ContactId = 100,
TenantId = 1
});
await (authProvider as SomeAuthenticationStateProvider).LoginAsync(new AuthenticationLogin { Username = userName }, 24 * 60);
}
};
public class CustomTokenStore : IUserAccessTokenStore
{
ConcurrentDictionary<string, UserAccessToken> _tokens = new ConcurrentDictionary<string, UserAccessToken>();
public Task ClearTokenAsync(ClaimsPrincipal user, UserAccessTokenParameters parameters = null)
{
var sub = user.FindFirst("sub").Value;
_tokens.TryRemove(sub, out _);
return Task.CompletedTask;
}
public Task<UserAccessToken> GetTokenAsync(ClaimsPrincipal user, UserAccessTokenParameters parameters = null)
{
var sub = user.FindFirst("sub").Value;
_tokens.TryGetValue(sub, out var value);
return Task.FromResult(value);
}
public Task StoreTokenAsync(ClaimsPrincipal user, string accessToken, DateTimeOffset expiration, string refreshToken = null, UserAccessTokenParameters parameters = null)
{
var sub = user.FindFirst("sub").Value;
var token = new UserAccessToken
{
AccessToken = accessToken,
Expiration = expiration,
RefreshToken = refreshToken
};
_tokens[sub] = token;
return Task.CompletedTask;
}
}

services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.ClaimActions.Add(new CustomClaimsFactory(
"userName",
"XXXXX#outlook.com"
));
options.TokenValidationParameters = new TokenValidationParameters
{
// Instead of using the default validation (validating against a single issuer value, as we do in
// line of business apps), we inject our own multitenant validation logic
ValidateIssuer = false,
// If the app is meant to be accessed by entire organizations, add your issuer validation logic here.
//IssuerValidator = (issuer, securityToken, validationParameters) => {
// if (myIssuerValidationLogic(issuer)) return issuer;
//}
};
options.Events = new OpenIdConnectEvents
{
OnTicketReceived = context =>
{
// If your authentication logic is based on users then add your logic here
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Response.Redirect("/Error");
context.HandleResponse(); // Suppress the exception
return Task.CompletedTask;
},
// If your application needs to do authenticate single users, add your user validation below.
//OnTokenValidated = context =>
//{
// var claims = new List<Claim>
// {
// new Claim(ClaimTypes.Role, "superadmin")
// };
// var appIdentity = new ClaimsIdentity(claims);
// context.Principal.AddIdentity(appIdentity);
// return Task.CompletedTask;
// //return myUserValidationLogic(context.Ticket.Principal);
//}
};
});
While CustomActionFactory class is defined below
public class CustomClaimsFactory : ClaimAction
{
string _ClaimType;
string _ValueType;
public CustomClaimsFactory(string claimType, string valueType) : base(claimType, valueType)
{
_ClaimType = claimType;
_ValueType = valueType;
}
public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
{
identity.AddClaim(new Claim(_ClaimType, _ValueType, issuer));
}
}```

OnTokenValidated has a method to add the new identity you create with the claim:
OnTokenValidated = ctx =>{ ctx.Principal.AddIdentity(myNewClaim); }

Related

HttpContext is null when send a request from axios

I am trying to fetch user from my backend, when I am send a request to my controller from backend them HttpContext.User is coming with data. But when I am trying to fetch user from frontend with axios, HttpContext.User is null and data is not returning.
Here is my get User method in Controller:
[HttpGet("me")]
public IActionResult GetMe()
{
var claims = HttpContext.User.Claims.Select(c => new { c.Type, c.Value }).ToList();
return Ok(claims);
}
Here is my authentication configuration in Program.cs:
builder.Services.AddAuthentication("cookie").AddCookie("cookie").AddOAuth("Discord", options =>
{
options.SignInScheme = "cookie";
options.ClientId = builder.Configuration.GetValue<string>("Discord:ClientId");
options.ClientSecret = builder.Configuration.GetValue<string>("Discord:ClientSecret");
options.AuthorizationEndpoint = "https://discord.com/oauth2/authorize";
options.TokenEndpoint = "https://discord.com/api/oauth2/token";
options.CallbackPath = "/oauth/discord-cb";
options.Scope.Add("identify");
options.SaveTokens = true;
options.UserInformationEndpoint = "https://discord.com/api/users/#me";
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "username");
options.Events.OnCreatingTicket = async ctx =>
{
var request = new HttpRequestMessage(HttpMethod.Get, ctx.Options.UserInformationEndpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", ctx.AccessToken);
var response = await ctx.Backchannel.SendAsync(request);
var user = await response.Content.ReadFromJsonAsync<JsonElement>();
ctx.RunClaimActions(user);
};
});
app.MapGet("/", (HttpContext ctx) =>
{
ctx.GetTokenAsync("access_token");
return ctx.User.Claims.Select(x => new { x.Type, x.Value }).ToList();
});
app.MapGet("/login", () =>
{
return Results.Challenge(new AuthenticationProperties
{
RedirectUri = "http://localhost:3000"
}, authenticationSchemes: new List<string>() { "Discord" });
});
I also set Cors Policy, and tried this:
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthController(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
[HttpGet("me")]
public IActionResult GetMe()
{
var claims = _httpContextAccessor.HttpContext.User.Claims.Select(c => new { c.Type, c.Value }).ToList();
return Ok(claims);
}
Backend is running on: https://localhost:5005/ (C# Asp.Net Core)
Fronend is running on: http://localhost:3000/ (Vue.js)
When I am go to this url: https://localhost:5005/api/auth/me this is the result:

Identity Server 4 - Custom Identity Claims are not showing in Id Token, but scopes are visible in Access Token

Requirement: Here I have MVC Client request IdentityServer for Id & access token using Auth-Code mechanism.
Infra: .NetCore = 3.1 & IdentityServer4 = 3.1.4
For this I have used 2 types of configurations.
Type1 - AlwaysIncludeUserClaimsInIdToken = true
Type2 - AlwaysIncludeUserClaimsInIdToken = false & options.ClaimActions.MapJsonKey() to
map the claims. & GetClaimsFromUserInfoEndpoint = true
When you see the code below, I have seggregated as 3 layers, first one is Type1 & sceond one is Type2 and common Config is for both Type1 & Type2(as it doesn't vary).
Common Configuration.
Identity Server Proj::
Program.cs //Add Claims when user is created.
public static void Main(string[] args)
{
//DI is ready when we call create host builder.
var host = CreateHostBuilder(args).Build();
// use DI to inject/seeding of new users.
using (var scope = host.Services.CreateScope())
{
var userManager = scope.ServiceProvider
.GetRequiredService<UserManager<IdentityUser>>();
userManager.CreateAsync(SeedingUsers.GetUser, "password").GetAwaiter().GetResult();
userManager.AddClaimsAsync(SeedingUsers.GetUser, new List<Claim>
{
new Claim("employee_id", "employee_id") //added employee_id as custom claim
}).GetAwaiter().GetResult();
}
host.Run();
}
startp.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(config =>
{
config.UseInMemoryDatabase("Memory");
});
services.AddIdentity<IdentityUser, IdentityRole>(config =>
{
config.Password.RequiredLength = 4;
config.Password.RequireDigit = false;
config.Password.RequireUppercase = false;
config.Password.RequireNonAlphanumeric = false;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(config =>
{
config.Cookie.Name = "Identity_Cookie"; //It holds logged-in user's data.
config.LoginPath = "/Accounts/Login";
});
services.AddIdentityServer()
.AddAspNetIdentity<IdentityUser>()
.AddInMemoryIdentityResources(MyConfiguration.GetIdentityResources)
.AddInMemoryApiResources(MyConfiguration.GetApiResources)
.AddInMemoryClients(MyConfiguration.GetClients)
.AddDeveloperSigningCredential();
services.AddControllersWithViews();
}
1. Type-1 Configuration.
Identity Server Proj::
MyConfiguration.cs //used to configure Identity.
public static class MyConfiguration
{
public static IEnumerable<Client> GetClients =>
new List<Client>
{
// Registering client for mvc
new Client
{
ClientId = "client_id_mvc",
ClientSecrets = new List<Secret>()
{
new Secret("client_secret_mvc".ToSha256())
},
RedirectUris = { "https://localhost:44362/signin-oidc" },
AllowedGrantTypes = GrantTypes.Code,
AllowedScopes = {
"Aum",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"custom.employee_profile", //custom Id-Scope
},
AlwaysIncludeUserClaimsInIdToken = true,
RequireConsent = false,
}
};
// This is used to craft the Id Token by IdentityServer.
public static IEnumerable<IdentityResource> GetIdentityResources =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource
{
Name = "custom.employee_profile",
UserClaims = {"employee_id"},
}
};
// This is used to craft the access Token by IdentityServer.
public static IEnumerable<ApiResource> GetApiResources =>
new List<ApiResource> {
new ApiResource("Aum")
};
}
MVC Client Proj::
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(config =>
{
config.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
config.DefaultChallengeScheme = "oidc";
config.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:44322/";
options.ClientId = "client_id_mvc";
options.ClientSecret = "client_secret_mvc";
options.SaveTokens = true;
options.ResponseType = "code";
options.GetClaimsFromUserInfoEndpoint = false; // process -1
//custom claims
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("custom.employee_profile"); //Requesting for custom scope.
});
services.AddControllersWithViews();
}
2. Type-2 Configuration.
Identity Server Proj::
MyConfiguration.cs //used to configure Identity.
public static class MyConfiguration
{
public static IEnumerable<Client> GetClients =>
// Registering client for mvc
new Client
{
ClientId = "client_id_mvc",
ClientSecrets = new List<Secret>()
{
new Secret("client_secret_mvc".ToSha256())
},
RedirectUris = { "https://localhost:44362/signin-oidc" },
AllowedGrantTypes = GrantTypes.Code,
AllowedScopes = {
"Aum",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"custom.employee_profile", //custom Id-Scope
},
AlwaysIncludeUserClaimsInIdToken = false, // process -2
RequireConsent = false,
}
};
// This is used to craft the Id Token by IdentityServer.
public static IEnumerable<IdentityResource> GetIdentityResources =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource
{
Name = "custom.employee_profile",
UserClaims = {"employee_id"},
},
};
// This is used to craft the access Token by IdentityServer.
public static IEnumerable<ApiResource> GetApiResources =>
new List<ApiResource> {
new ApiResource("Aum")
};
}
MVC Client Proj::
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(config =>
{
config.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
config.DefaultChallengeScheme = "oidc";
config.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:44322/";
options.ClientId = "client_id_mvc";
options.ClientSecret = "client_secret_mvc";
options.SaveTokens = true;
options.ResponseType = "code";
//used for mapping custom clian in Identity to Client.
options.ClaimActions.DeleteClaim("amr");
options.ClaimActions.DeleteClaim("s_hash");
// process -2 This will map Identity claims to current mvc client client.
options.ClaimActions.MapJsonKey("employee_code", "employee_id");
//used for round trips to the Identity, This will hit /userInfo endpoint to get Id token.
options.GetClaimsFromUserInfoEndpoint = true; // process -2
//custom claims
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("custom.employee_profile");
});
services.AddControllersWithViews();
}
Expected result:
Id_token with scope employee_id claim (scope of custom.employee_profile).
Actual:
Missing employee_id claim in Id_token

Reloading page affecting sign out of user/log out

I have been trying to getting this fixed for many times already, but have no success so far. I am logging into my page and everything works fine. However every time I am reloading my page I am ending up signed out and redirected to "Login page" and have to sign in again. What is the problem? Is there is something messed up with my Coockie logic?
I have also tried to implement login.RememberMe logic, but it does not work either. I have checked that login.RememberMe returns true, but no effect.
Login method in controller:
[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginModel login)
{
ApplicationUser user = await this.SignInManager.UserManager.FindByEmailAsync(login.Email);
Microsoft.AspNetCore.Identity.SignInResult result =
await this.SignInManager.PasswordSignInAsync(login.Email, login.Password, login.RememberMe, false);
if (!result.Succeeded)
{
List<string> errors = new List<string>();
errors.Add("Email and password are invalid.");
return BadRequest(new LoginResult
{
Successful = false,
Errors = errors,
});
}
IList<string> roles = await this.SignInManager.UserManager.GetRolesAsync(user);
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, login.Email)
};
ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
AuthenticationProperties props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMonths(1)
};
// to register the cookie to the browser
this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props).Wait();
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Configuration["JwtSecurityKey"]));
SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
DateTime expiry = DateTime.Now.AddDays(Convert.ToInt32(this.Configuration["JwtExpiryInDays"]));
JwtSecurityToken token = new JwtSecurityToken(
this.Configuration["JwtIssuer"],
this.Configuration["JwtAudience"],
claims,
expires: expiry,
signingCredentials: creds
);
return Ok(new LoginResult
{
Successful = true,
Token = new JwtSecurityTokenHandler().WriteToken(token),
});
}
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
};
})
.AddCookie(options =>
{
options.Cookie.Name = "DashCoockie";
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
options.EventsType = typeof(CookieAuthEvent);
});
services.AddScoped<CookieAuthEvent>();
services.AddAuthorization(config =>
{
config.AddPolicy(Policies.IsAdmin, Policies.IsAdminPolicy());
config.AddPolicy(Policies.IsUser, Policies.IsUserPolicy());
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
services.AddControllers();
// Instant update on runtime for development purposes
services.AddControllersWithViews().AddRazorRuntimeCompilation();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
app.UseCookiePolicy();
}
CookieAuthEvent.cs:
public class CookieAuthEvent : CookieAuthenticationEvents
{
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
context.Request.HttpContext.Items.Add("ExpiresUTC", context.Properties.ExpiresUtc);
}
}
After logging in (Coockie name is the same, just hided first part as it is in the code above):
My project structure is as follows: Client, Server, Shared. I found solution by using LocalStorage and AuthService : IAuthService on Client side. So is it so that in my setup the only solution is to use LocalStorage?
public class AuthService : IAuthService
{
private readonly HttpClient _httpClient;
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly ILocalStorageService _localStorage;
public AuthService(HttpClient httpClient,
AuthenticationStateProvider authenticationStateProvider,
ILocalStorageService localStorage)
{
_httpClient = httpClient;
_authenticationStateProvider = authenticationStateProvider;
_localStorage = localStorage;
}
public async Task<RegisterResult> Register(RegisterModel registerModel)
{
var result = await _httpClient.PostJsonAsync<RegisterResult>("api/accounts", registerModel);
return result;
}
public async Task<LoginResult> Login(LoginModel loginModel)
{
var loginAsJson = JsonSerializer.Serialize(loginModel);
var response = await _httpClient.PostAsync("api/Login", new StringContent(loginAsJson, Encoding.UTF8, "application/json"));
var loginResult = JsonSerializer.Deserialize<LoginResult>(await response.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (!response.IsSuccessStatusCode)
{
return loginResult;
}
await _localStorage.SetItemAsync("authToken", loginResult.Token);
((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsAuthenticated(loginModel.Email);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", loginResult.Token);
return loginResult;
}
public async Task Logout()
{
await _localStorage.RemoveItemAsync("authToken");
((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsLoggedOut();
_httpClient.DefaultRequestHeaders.Authorization = null;
}
}
It seems that you didn't set the token in your request header
try add these codes in your startup class:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["access_token"];
return Task.CompletedTask;
}
};
});
In your Login Action :
Response.Cookies.Append("access_token",your JwtSecurityToken)

AddJwtBearer + AddAzureADBearer Not working together

I am writing a authentication piece (.Net Core 2.1) where it can make use of either a JWT from a service provider or a token from AzureAD. I have written the following test to understand what is required , but it seems as if the first authentication provider is always hit when I test with Postman and send through a bearer token.
I have put a break point on both bearer providers events, if i call the values controller with the bearer token it hits the "first" one even though i have specified to use the AuthenticationScheme of "second"
Startup.cs
services.AddAuthentication("first")
.AddJwtBearer("first", options =>
{
options.Authority = "https://123";
options.TokenValidationParameters =
new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidAudiences = new[] { "123" }
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var accesToken = context.SecurityToken;
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
var accessToken = context.Principal;
return Task.CompletedTask;
},
};
})
.AddAzureADBearer("second",
AzureADDefaults.JwtBearerAuthenticationScheme,
options => Configuration.Bind("AzureAd", options));
// Added
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme,
options =>
{
//Configuration.Bind("AzureAd", options);
//options.Authority += "/v2.0";
options.TokenValidationParameters.ValidAudiences = new[] { options.Audience, $"api://{options.Audience}" };
options.TokenValidationParameters.ValidateIssuer = true;
options.IncludeErrorDetails = true;
options.TokenValidationParameters.ValidateLifetime = false;
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var accesToken = context.SecurityToken;
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
var accessToken = context.Principal;
return Task.CompletedTask;
}
};
});
ValuesController.cs
[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = "second")]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
}
any help will be appreciated.
Try using policies to enable AuthenticationSchemes
We use something like this (didn't try running the code)
services
.AddPolicyScheme("first", "First policy selector", options =>
{
options.ForwardDefaultSelector = context =>
{
return "first"
};
})
.AddPolicyScheme("second", "Second policy selector", options =>
{
options.ForwardDefaultSelector = context =>
{
return "second"
};
})
.AddAuthentication("first")
.AddJwtBearer("first", options =>
{
options.Authority = "https://123";
options.TokenValidationParameters =
new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidAudiences = new[] { "123" }
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var accesToken = context.SecurityToken;
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
var accessToken = context.Principal;
return Task.CompletedTask;
},
};
})
.AddAzureADBearer("second",
AzureADDefaults.JwtBearerAuthenticationScheme,
options => Configuration.Bind("AzureAd", options));
// Added
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme,
options =>
{
//Configuration.Bind("AzureAd", options);
//options.Authority += "/v2.0";
options.TokenValidationParameters.ValidAudiences = new[] { options.Audience, $"api://{options.Audience}" };
options.TokenValidationParameters.ValidateIssuer = true;
options.IncludeErrorDetails = true;
options.TokenValidationParameters.ValidateLifetime = false;
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var accesToken = context.SecurityToken;
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
var accessToken = context.Principal;
return Task.CompletedTask;
}
};
});

ApiController does not receive Identity info from HttpRequest (Razor)

It is my first time to use ASP.NET core 2.2 Identity to manage Authorization in Controller. In the client side, I use (Login.cshtml.cs) SignInManger.PasswordSignInAsync(…) to sign in a user. After user sign in successfully, I send HttpClient call to UsersController in another project to get more user’s info. However, the Controller is does not receive the the Claims.ClaimsPrincipal (User) information from the Request. The [Authorize(Roles = “admin”)] fails but [AllowAnonymous] passes. I checked the Request cookie. The User HttpContext is not updated to the signed in user in client side. To be exact, I don’t know how to update the User in cookie. I thought it is done in the background. I have looked through many posts but none of them explicitly mentions how the User is being updated in the cookie and send to the controller. It seems that the User updated is done after the program exits the Login.cshtml.cs. I am able to get the Identity of the user by using SingInManager.CreateUserPrincipalAsync(…) within public async Task OnPostAsync(string returnUrl = null) of Login.cshtml.cs. But I don’t know how to pass that Identity to the User in cookie and send it to Controller.
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
// test: get the Identity of the Input.Email
var user = await _signInManager.UserManager.FindByEmailAsync(Input.Email);
var userPrincipal = await _signInManager.CreateUserPrincipalAsync(user);
var identity = userPrincipal.Identity;
// * User Identity does not populate at this point.
var ret = await GetRestOfUserInfo();
}
}
return Page();
}
private async Task<int> GetRestOfUserInfo()
{
var userStatus = await _userManager.GetUserAsync(User);
UserCSDto userCDDto = new UserCSDto()
{
Email = Input.Email,
UserName = Input.Email,
Password = Input.Password,
RememberMe = Input.RememberMe
};
CookieContainer cookieContainer = new CookieContainer();
HttpClientHandler handler = new HttpClientHandler
{
UseCookies = true,
UseDefaultCredentials = true,
CookieContainer = cookieContainer
};
var client = new HttpClient(handler);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var content = new StringContent(JsonConvert.SerializeObject(userCDDto), Encoding.UTF8, "application/json");
var url = _connectionSettings.UrlConnection + "/api/users/authenticate";
var response = await client.PostAsync(url, content);
if (response.IsSuccessStatusCode)
{
var respUser = JsonConvert.DeserializeObject<User>(response.Content.ReadAsStringAsync().Result);
var _responseToken = response.Headers.Where(t => t.Key == "Authorization")
.Select(t => t.Value);
var token = (_responseToken.FirstOrDefault()).FirstOrDefault();
applicationUser.PreferredName = respUser.PreferredName;
applicationUser.Token = token;
ApplicationUser user = await _userManager.FindByEmailAsync(Input.Email);
user.Token = token;
user.PreferredName = respUser.PreferredName ?? "";
IEnumerable<Claim> claims = new Claim[]
{
new Claim("Token", token),
new Claim("PreferredName", "123abc") // respUser.PreferredName)
};
await _userManager.AddClaimsAsync(user, claims);
return 1;
}
return 0;
}
[Authorize]
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class UsersController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private IUserService _userService;
public UsersController(UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
:
:
[Authorize(Roles = "any role")] // failed because User does not contain any info
[AllowAnonymous] // works because no authorization check
[HttpPost("authenticate")]
public async Task<IActionResult> Authenticate([FromBody]UserCSDto userParam)
{
var username = User.Identity.Name; // username is null because User does not
// have info.
var user = _userService.Authenticate(userParam.UserName, userParam.Password);
if (user == null)
return BadRequest(new { message = "Username or passwrod is incorrect" });
var token = _jwtTokenService.CreateToken(userCSDto);
Response.Headers.Add("Authorization", "Bearer " + token);
Response.Headers.Add("Content-Type", "application/json");
return Ok(userCSDto);
}
:
:
}
/////////////////////////////
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfiguration>(Configuration);
services.Configure<ConnectionSettings>(Configuration.GetSection("ConnectionStrings"));
var connection = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<EFCoreContext>(
options => options.UseSqlServer(connection,
b => b.MigrationsAssembly("EFStructures"))); // from Data project
services.AddHttpContextAccessor(); // #.Net Core 2.2
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.Expiration = TimeSpan.FromHours(1);
options.SlidingExpiration = true;
});
services.AddSession(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.Name = "Test.Session";
options.IdleTimeout = TimeSpan.FromMinutes(60);
options.Cookie.Path = "/";
});
services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+";
options.User.RequireUniqueEmail = true;
// Require Confirmed and Unique Email
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = false;
});
#region authenication using JWT token
services.Configure<JwtTokenSettings>(Configuration.GetSection("JwtTokenSettings"));
///// configure jwt authentication
var jwtTokenSettings = Configuration.GetSection("JwtTokenSettings").Get<JwtTokenSettings>();
var key = Encoding.ASCII.GetBytes(jwtTokenSettings.Secret);
//Adds cookie middleware to the services collection and configures it
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => options.LoginPath = new PathString("/account/login"));
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
//ValidateIssuer = Configuration["AppSettings:Issuer"],
//ValidateAudience = Configuration["AppSettings:Issuer"],
ValidateLifetime = true
};
});
#endregion
#region Add Role services to Identity RAZOR PAGE only
services.AddIdentity<ApplicationUser, IdentityRole>()
// .AddDefaultUI(Microsoft.AspNetCore.Identity.UI.UIFramework.Bootstrap4)
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<EFCoreContext>();
#endregion
#region Setting for using Identity
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
#endregion Setting for using Identity
services.AddHttpClient();
services.AddScoped<IJwtTokenService, JwtTokenService>();
services.AddScoped<IEmailSenderService, EmailSenderService>();
services.AddScoped<IEmailSender, EmailSender>();
#region Get EmailSettings from appsettings.json
services.AddOptions();
services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));
#endregion Get EmailSettings from appsettings.json
services.AddMvc()
.ConfigureApiBehaviorOptions(options =>
{
options
.SuppressUseValidationProblemDetailsForInvalidModelStateResponses = true;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseAuthentication();
app.UseSession();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRequestLocalization();
app.UseMvcWithDefaultRoute();
// global cors policy
app.UseCors(x => x
.WithOrigins("https://localhost")
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseMvc();
}
}

Categories