GetAuthorizationContextAsync(returnUrl) returns null but I have no idea what I'm missing - asp.net-core

I am trying to implement an Identity Server Solution that redirects the user to different Login Views depending on the client application they come from.
To do this, in the AccountController.cs file I have the following method:
private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl)
{
var isValid = this.interaction.IsValidReturnUrl(returnUrl);
var context = await this.interaction.GetAuthorizationContextAsync(returnUrl);
var schemes = await this.schemeProvider.GetAllSchemesAsync();
return new LoginViewModel
{
AllowRememberLogin = AccountOptions.AllowRememberLogin,
ReturnUrl = returnUrl,
Username = context?.LoginHint
};
}
I have set up a Configuration & Operational DbContexts as per this tutorial from the IdentityServer4 documentation.
Additionally, I have seeded the database with some rows in the Clients & ClientRedirectUris tables.
Presumably, that should be all I need to access the AuthorizationContext from the IIdentityServerInteractionService API, but the method above always returns null, and the isValid variable is always false too.
I have made sure that the returnUrl I am passing in is exactly the same as the redirectUri stored in my database (I am using localhost and running all this locally, if that matters)
Can someone please help? I have no idea what I'm doing wrong...

You have to do like this:
Html.BeginForm("Login", "Account", new {ReturnUrl = Request.QueryString["ReturnUrl"] })
[HttpPost]
public async Task<IActionResult> Login(LoginInputModel model, string ReturnUrl) {
...
}

Related

Blazor WASM AuthenticationState using AAD - Claims null until Refresh

New to Blazor and have been doing a hatchet job to get things working how I want.
I am using Blazor WASM with AAD for Authentication created based on this document MS Doc. I implemented the SecureAccountFactory class from the example and call a db where I get the associated user based on the AAD Guid, then add everything into Claims.
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(SecureUserAccount account,
RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);
if (initialUser.Identity.IsAuthenticated)
{
var userIdentity = (ClaimsIdentity)initialUser.Identity;
var claims = userIdentity.Claims;
var principalId = claims.Where(x => x.Type == "oid").First();
//Get some user info from SQL
var User = await _UserService.Get(principalId.Value);
//Get user Roles from SQL and add to Claims
var UsersInRoles = await _UsersInRoleService.RolesByUserId(principalId.Value);
//Add the ClientId to Claims
userIdentity.AddClaim(new Claim("clientId", User.ClientId.ToString()));
foreach (var userrole in UsersInRoles)
{
userIdentity.AddClaim(new Claim("appRole", userrole.Role.Name));
}
}
return initialUser;
}
I then have a Profile Component that appears on every page as part of the MainLayout which should have some info about the current user, so I made a static class to retrieve this info.
public static class UserHelper
{
public static async Task<CurrentUserClaims> GetCurrentUserClaims(Task<AuthenticationState> authenticationStateTask)
{
AuthenticationState authenticationState;
authenticationState = await authenticationStateTask;
var AuthenticationStateUser = authenticationState.User;
var user = authenticationState.User;
var claims = user.Claims;
var clientClaim = claims.Where(x => x.Type == "clientId").First();
var principalId = claims.Where(x => x.Type == "oid").First();
return new CurrentUserClaims
{
ClientId = Convert.ToInt32(clientClaim.Value),
PrincipalId = Guid.Parse(principalId.Value),
user = user
};
}
}
In my ProfileComponent, I call CascadingParameter and then onParametersSet I query my Static class for the info from the current logged in user
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private string profilePath;
protected override async Task OnParametersSetAsync()
{
CurrentUserClaims UserClaims = await UserHelper.GetCurrentUserClaims(authenticationStateTask);
var principal = UserClaims.PrincipalId;
//... do stuff
}
The above all works, after a Refresh or once I route to any other page. The initial Load, after login on the home page shows that the below line always fails with 'Sequence contains no elements'
var clientClaim = claims.Where(x => x.Type == "clientId").First();
I am using Authorize to protect the pages and I will eventually be using the Roles to determine what to display to the user.
A: Surely there's a better way of doing the above. There are lots and lots of articles on creating a custom Auth which inherits AuthenticationState but every one I've seen adds the Claims manually as a fake user, so I don't see how to access the actual Claims.
B: I'm wondering if just using LocalStorage for the User info might be a simpler way to go but is it considered 'safe' or best practice?
Any pointers to a solution are appreciated.

How can I read a JWT Token on an [AllowAnonymous] EndPoint in EF .NET Core

I have an endpoint that I want public but also perform some actions if the user is logged in. If I add an AuthorizeAttribute, the claims are there, but if I make it AllowAnonymous, the claims are empty.
[HttpGet]
[Authorize("Read")] //I want this to be AllowAnonymous
public async Task<ActionResult<List<string>>> Get()
{
//This only works if I use [Authorize("Read")]
var subject = context.HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
return Ok(new List<string>() { "A", "B" });
}
I hope I explained that clear enough.
HttpContext.User is not available when you are using AllowAnonymous (it's null). You can however hack around this by getting the ClaimsPrincipal using the AuthenticateAsync() method. Be sure to change the AuthenticationScheme to whatever scheme you are using (this example is JWT).
var auth = await HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
if (auth.Succeeded)
{
var claimsPrincipal = auth.Principal;
var subject = claimsPrincipal.Claims.FirstOrDefault(a => a.Type == ClaimTypes.NameIdentifier);
// use the subject claim as needed (actual value is "subject.Value")
}
I have used this code on Asp.Net Core 2 and 3.1 using JWT, so your mileage may vary if you use something else.

ASP.NET Identity with Sustainsys Saml2 - How to persist ExternalLoginInfo Claims?

I have an ASP.NET Core app, targeting netcoreapp3.1, set up with ASP.NET Identity and the Sustainsys.Saml2.AspNetCore2 package.
IDP-initiated SAML authentication is working fine, but I can't retrieve custom attributes/claims from the signed-in user after the authentication redirect.
In my ExternalLogin.cshtml.cs class, the custom claims are present on the ExternalLoginInfo.Principal (as var info in the code below), but they are not retrievable from Context.User.Claims after the redirect.
That is, _logger.LogInformation($"PatientId: {info.Principal.FindFirst("PatientId")}"); prints the value passed in the custom PatientId SAML attribute, but #Context.User.FindFirst("PatientId"); is null after the redirect.
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
var info = await _CustomSignInManager.GetExternalLoginInfoAsync();
if (info == null)
{
ErrorMessage = "Error loading external login information.";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _CustomSignInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation($"{info.Principal.Identity.Name} logged in with {info.LoginProvider} provider.");
_logger.LogInformation($"PatientId: {info.Principal.FindFirst("PatientId")}");
return LocalRedirect(returnUrl);
}
...
}
My conclusion from this answer is that these claims should still be available. Do I need to somehow pass the ExternalLoginInfo.Principal (rather just the LoginProvider and ProviderKey) to the ExternalLoginSignInAsync method?
I will say that I only want to persist the claims for the length of the session, not add them as AspNetUserClaims the database. They will be different on each login.
I figured out something that works. My question at the end there, "Do I need to somehow pass the ExternalLoginInfo.Principal (rather just the LoginProvider and ProviderKey) to the ExternalLoginSignInAsync method?" was on the right track.
As you can see above, the ExternalLoginSignInAsync method is in the CustomSignInManager class (it's a custom implementation for reasons not relevant to this question, but that method was unchanged from the default ASP.NET Identity SignInManager).
Tracing the request path to the "bottom," ExternalLoginSignInAsync calls SignInOrTwoFactorAsync, which calls SignInAsync. In SignInAsync, a new ClaimsIdentity is created and returned. So I simply had to pass them from the ExternalLoginInfo.Principal instance through all of those methods such that I could set them on the new principal.
Potentially helpful minutiae: I chose to create a CustomClaimSet class to store the custom attributes, and then I had to update all the method signatures to accept a CustomClaimSet as an argument.

Add RememberMe value as Claim in JWT (Identity Server 4)

I'm using IdentityServer 4.
Is it possible to access the value of the RememberMe boolean when issuing claims? (named isPersistent in the Microsoft.AspNetCore.Identity)
My idea is to add a claim reflecting the RememberMe value so that other applications can use the value.
Currently I'm adding my Claims in the implementation of the interface IProfileService.GetProfileDataAsync.
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
await Task.Run(() =>
{
try
{
var user = _userManager.GetUserAsync(context.Subject).Result;
var claims = new List<Claim>
{
// I'm adding my current claims here, like so:
new Claim("contact_id", user.ContactId.ToString()),
// etc
// I would like to add RememberMe
new Claim("remember_me", ??? )
};
context.IssuedClaims.AddRange(claims);
// ..
Or can the RememberMe value be accessed by some other method?
You can add additional claims during the user's login. There is an overload for SignInAsync which accepts an array of additional claims.
Here is a code snippet.
public async Task<IActionResult> Login(LoginInputModel model)
...
AuthenticationProperties props = null;
Claim keepMeLoggedIn = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
keepMeLoggedIn = new Claim(AccountOptions.KeepLoggedInClaim, true.ToString());
}
await HttpContext.SignInAsync(userId.ToString(), model.Username, props, keepMeLoggedIn);
Please note that to make this solution work it's necessary to insert your claim name to the IdentityClaims table.
Yes , you should add claim to tokens . In standard OIDC specifications, token is the
bond between client and identity provider . The profile service is called whenever IdentityServer needs to return claims about a user to a client applications , and could be used to add your custom claims .
http://docs.identityserver.io/en/latest/reference/profileservice.html

Identity Server 4 User Impersonation

I am struggling to implement a Impersonation feature into the Identity Server 4 Service. I understand that there's a lot of people who are against implementing it the way I want to but I really need the full redirect back to the SSO server in order to generate a new list of claims. The user that is being impersonated will have a completely different set of Rules associated with them as claims, so it must come from the IdSrvr.
I have read through https://github.com/IdentityServer/IdentityServer4/issues/853, and also IdentityServer4 - How to Implement Impersonation
Here's what I've attempted so far, We did this perfectly inside of Identity Server 3 with ACR values and the Pre-Auth even on the UserService.
This controller method I am calling from one of the Clients of my identity server:
public IActionResult Impersonate(string userIdToImpersonate, string redirectUri)
{
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(){RedirectUri = redirectUri, Items = { {"acr_values", $"userIdToImpersonate:{userIdToImpersonate}"}}});
}
Here is my OnRedirectToProvider:
OnRedirectToIdentityProvider = context =>
{
if (context.Properties.Items.ContainsKey("acr_values"))
{
context.ProtocolMessage.AcrValues = context.Properties.Items["acr_values"].ToString();
}
return Task.CompletedTask;
}
This is where i start to get lost, at the moment, I've inherited from the AuthorizeInteractionResponseGenerator class and implemented my own with the override to the ProcessLoginAsync (this is the only thing i could find that was close to the pre-auth event previously)
protected override async Task<InteractionResponse> ProcessLoginAsync(ValidatedAuthorizeRequest request)
{
if (!request.IsOpenIdRequest) return await base.ProcessLoginAsync(request);
var items = request.GetAcrValues();
if (items.Any(i => i.Contains("userIdToImpersonate")) && request.Subject.IsAuthenticated())
{
//handle impersonation
var userIdToImpersonate = items.FirstOrDefault(m => m.Contains("userIdToImpersonate")).Split(':').LastOrDefault();
request.Subject = await _signInManager.ImpersonateAsync(userIdToImpersonate);
//var userToImpersonate = await _signInManager.UserManager.FindByIdAsync(userIdToImpersonate);
//if (userToImpersonate == null) return await base.ProcessLoginAsync(request);
//var userBeingImpersonated = await _signInManager.UserManager.FindByIdAsync(userIdToImpersonate);
//var currentUserIdentity = await _signInManager.CreateUserPrincipalAsync(userBeingImpersonated);
//var currentClaims = currentUserIdentity.Claims.ToList();
//currentClaims.Add(new Claim(IdentityServiceClaimTypes.ImpersonatedById, request.Subject.IsBeingImpersonated() ? request.Subject.GetClaimValue(IdentityServiceClaimTypes.ImpersonatedById) : _signInManager.UserManager.GetUserId(request.Subject)));
//request.Subject = new ClaimsPrincipal(new ClaimsIdentity(currentClaims));
//return await base.ProcessLoginAsync(request);
return new InteractionResponse();
}
else
{
return await base.ProcessLoginAsync(request);
}
}
As you can see, i've tried a couple different things here, When not using OIDC as a authentication scheme, and my IdServer/Site is the same site, I had a function that impersonation worked with. Which is where _signInManager.ImpersonateAsync(...) is. Here is that Implementation:
public async Task<ClaimsPrincipal> ImpersonateAsync(string userIdToImpersonate)
{
var userBeingImpersonated = await UserManager.FindByIdAsync(userIdToImpersonate);
if (userBeingImpersonated == null) return null;
var currentUserIdentity = await CreateUserPrincipalAsync(userBeingImpersonated);
var currentClaims = currentUserIdentity.Claims.ToList();
currentClaims.Add(new Claim(IdentityServiceClaimTypes.ImpersonatedById, Context.User.IsBeingImpersonated() ? Context.User.GetClaimValue(IdentityServiceClaimTypes.ImpersonatedById) : UserManager.GetUserId(Context.User)));
//sign out current user
await SignOutAsync();
//sign in new one
var newIdentity = new ClaimsPrincipal(new ClaimsIdentity(currentClaims));
await Context.SignInAsync(IdentityConstants.ApplicationScheme, newIdentity);
return Context.User;
}
In an effort to simply 'replace' who was signing in, or at least who the identity server was thinking was signing in, i just replaced Request.Subject with the Impersonation Result. This doesn't actually change anything that I can find, at least not on my client app. If i use the redirect URI of 'https://localhost:44322/signin-oidc' (localhost because i'm running the sites locally), I get a "Correlation failed at signin-oidc redirect" message. If anyone has implemented something like this or done anything similar I would greatly appreciate the help getting this complete.
Suggestions welcome for completely different implementations, this was just my best stab at what worked flawlessly with idsrvr3.