IdentityServer4 Windows Sign-On under IIS 10 fails to authenticate successfully - c#

Using IIS Express on my local machine, I'm able to run the IdentityServer4 QuickStart UI project and successfully sign in. However, once it is deployed to production, I'm unable to get it to work.
On the Application Pool for the site, I have a domain account setup (with just about every permission possible granted). I have tried every variation of having "anonymous authentication" toggled. I've gone as far as recreating the entire application from scratch in multiple different ways (no-SSL, only-SSL, fully open CORS, all security policies disabled), and even the most basic version of the application seems to suffer from the exact same issue.
After slapping some logging on the application, I can see that I'm grabbing the Subject ID and name from AD just fine.
Here's the ProcessWindowsLoginAsync method, with only minimal logging changes.
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
var props = new AuthenticationProperties
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
}
};
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
var sub = wp.FindFirst(ClaimTypes.PrimarySid).Value;
id.AddClaim(new Claim(JwtClaimTypes.Subject, sub));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
_logger.LogInformation("Assigning claims. Subject {#Subject}. Name {#Name}", sub, wp.Identity.Name);
if (AccountOptions.IncludeWindowsGroups)
{
var wi = wp.Identity as WindowsIdentity;
var groups = wi!.Groups!.Translate(typeof(NTAccount));
var roles = groups!.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
return Redirect(props.RedirectUri);
}
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
The above code spits out something akin to (with identifying information stripped):
Assigning claims. Subject S-0-0-00-0000000000-0000000000-0000000000-00000. Name DOMAIN\NAME
Once the above has executed, the external callback method is called and it immediately throws an exception:
[HttpGet]
public async Task<IActionResult> Callback()
{
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
_logger.LogInformation("We were not successfully able to sign in. Failure: {#Failure}. None: {#None}", result?.Failure, result?.None);
if (result?.Failure != null)
throw result.Failure;
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {#claims}", externalClaims);
}
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
if (user == null)
user = AutoProvisionUser(provider, providerUserId, claims);
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
var issuer = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(issuer, localSignInProps);
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.ClientId));
if (context != null)
if (await _clientStore.IsPkceClientAsync(context.ClientId))
return this.LoadingPage("Redirect", returnUrl);
return Redirect(returnUrl);
}
From the logs, I can tell that it's immediately failing after attempting to authenticate. There's no other errors, but a few interesting logs of note (in order):
Performing protect operation to key {xxxxxxxx-xxxx-xxxx-xxxx-b7e4d6dd250a} with purposes ('C:\websites\identity.ourdomain.com', 'Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware', 'idsrv.external', 'v2').
AuthenticationScheme: idsrv.external signed in.
Executing RedirectResult, redirecting to /External/Callback
Executing action method IdentityServer4.Quickstart.UI.ExternalController.Callback (Idsvr.Api) - Validation state: Valid
AuthenticationScheme: idsrv.external was not authenticated.
(Exception)

One of the possible root cause is that the callback cookie doesn't set properly.
Try to capture the network traffic, and check if idsrv.external cookie has been set correctly during Challenge.
In my case, setting cookie failed because SameSite=None is there without Secure=true.

Related

ASP.NET Identity - Get Saved third-party access tokens

I have an app that will operate almost entirely on Spotify OAuth, that will have features to alter the playback of your music.
I'm able to get Spotify OAuth working perfectly such that I can log into my app, but after I've logged in, I need access to the current user's spotify access_token so that I can forward it to my spotify requests.
I followed this guide from ms to try to save the tokens: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/?view=aspnetcore-6.0&tabs=visual-studio
And I have tried all these ways to then save that token into the HttpContext such that I can access it:
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
var spotifyAccessToken = tokens.FirstOrDefault(x => x.Name == "access_token").Value;
tokens.Add(new AuthenticationToken()
{
Name = "SpofityAccessToken",
Value = spotifyAccessToken
});
//store all the tokens as directed by MS
ctx.Properties.StoreTokens(tokens);
//store the properties into the HttpContext in 2 different ways
ctx.HttpContext.Items["Properties"] = ctx.Properties;
ctx.HttpContext.Features.Set(ctx.Properties);
//try adding a claim to the user
ctx.Identity.AddClaims(new[] { new Claim("SpotifyAccessToken", spotifyAccessToken) });
return Task.CompletedTask;
};
The problem I'm having is how do I then get this token out? all of these methods are not working:
[HttpGet]
public async Task Get()
{
await HttpContext.SignInAsync(User);
// try to re-run Authenticate, even though i'm already in an [Authorize] controller
var res = await HttpContext.AuthenticateAsync();
//props2 does not have the tokens i set
var props2 = res.Properties;
//props comes back null
var props = HttpContext.Features.Get<AuthenticationProperties>();
//claims has no SpotifyAccessToken claim
var claims = User.Claims.ToList();
var token = "hard-coded";
//here is where i need the token to then query spotify
var client = new SpotifyAPI.Web.SpotifyClient(token);
var res2 = await client.Player.GetCurrentPlayback();
}
I feel like I've tried everything, what am i doing wrong?
This is in a .NET 6 blazor wasm, .net core hosted app.
Also tried the solutions here to no avail Get AuthenticationProperties in current HttpRequest after HttpContext.SignInAsync
signInManager.UpdateExternalAuthenticationTokensAsync adds the the authentication tokens in [dbo].[AspNetUserTokens]
External login is where I call it:
// Sign in the user with this external login provider if the user already has a login.
var signInResult = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: true, bypassTwoFactor: true);
if (signInResult.Succeeded)
{
await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
_logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
return LocalRedirect(returnUrl);
}
Later on you can get it by using :
var token = await userManager
.GetAuthenticationTokenAsync(user, "Spotify", "access_token");
var expiresAtStr = await userManager
.GetAuthenticationTokenAsync(user, "Spotify", "expires_at");
If the token is stored in the Cookie then you can access the various tokens using:
string accessToken = await HttpContext.GetTokenAsync("access_token");
string idToken = await HttpContext.GetTokenAsync("id_token");
string refreshToken = await HttpContext.GetTokenAsync("refresh_token");
string tokenType = await HttpContext.GetTokenAsync("token_type");
string accessTokenExpire = await HttpContext.GetTokenAsync("expires_at");
However, you can not store data in ctx.HttpContext and assume it will be persisted across requests. either you sign-in the user using the cookie middleware or you store the tokens in the UserSession object.
See this article on how to configure and store data in the session, that will be persisted across requests.
Session and state management in ASP.NET Core
If you configure it properly, then you can use it like:
HttpContext.Session.SetString("token", token.Trim());

IdentityServer shows blank PostLogoutRedirectUri for Android Native App

I have created an OAuth Server using IdentityServer4 and .Net Core Signin Manager. The Login works great and returns to my app. The Logout doesn't seem to know who is logging out. The Logout Razor Page code is as follows:
public async Task<IActionResult> OnGet(string logoutId)
{
var logout = await _interaction.GetLogoutContextAsync(logoutId);
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri;
AutomaticRedirectAfterSignOut = (PostLogoutRedirectUri != null);
ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName;
SignOutIframeUrl = logout?.SignOutIFrameUrl;
LogoutId = logoutId;
if (User?.Identity.IsAuthenticated == true)
{
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider)
{
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
if (providerSupportsSignout)
{
if (LogoutId == null)
{
// if there's no current logout context, we need to create one
// this captures necessary info from the current logged in user
// before we signout and redirect away to the external IdP for signout
LogoutId = await _interaction.CreateLogoutContextAsync();
}
ExternalAuthenticationScheme = idp;
}
}
// delete local authentication cookie
await _signInManager.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
string url = Url.Action("Logout", new { logoutId = LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, ExternalAuthenticationScheme);
}
if (AutomaticRedirectAfterSignOut)
return Redirect(PostLogoutRedirectUri);
else
return Page();
}
When it gets called, there is a logoutId. It gets the context, but PostLogoutRedirectUri is blank. ClientId and ClientName are also blank, but the context has a field called ClientIds and the first entry is the correct ClientId for my app. The log shows as follows:
IdentityServer4.Validation.EndSessionRequestValidator: Information: End session request validation success
{
"SubjectId": "6841dc6c-0bd7-4f72-8f1c-f7czzzzzzzzz",
"Raw": {
"post_logout_redirect_uri": "mps.mobile.app://callback"
}
}
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.EndSessionCallbackEndpoint for /connect/endsession/callback
IdentityServer4.Endpoints.EndSessionCallbackEndpoint: Information: Successful signout callback.
I am using IdentityModel for the Client App. I have the logout coded as follows:
_options = new OidcClientOptions
{
Authority = MPSOidc.Authority,
ClientId = MPSOidc.ClientID,
Scope = "openid profile myapi offline_access email",
RedirectUri = MPSOidc.RedirectUri,
PostLogoutRedirectUri = MPSOidc.RedirectUri,
ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect,
Browser = new ChromeCustomTabsBrowser(this)
};
var oidcClient = new OidcClient(_options);
var r = new LogoutRequest();
await oidcClient.LogoutAsync(r);
It seems like the PostLogoutRedirectUri should show up here. Does anyone know a way to make this happen? If not, can the ClientId be used to get the Client information to find the PostLogoutRedirectUri there?
Thanks,
Jim
Here is what it was. When I logged out on the OidcClient, I didn't pass the ID Token. On my client Android app, I had to add the ID Token to the logout request:
var r = new LogoutRequest()
{
IdTokenHint = MPSOidc.Tokens.IdentityToken
};
That's all it took.
Cheers.

Windows Authentication with IdentityServer4 on IIS

I am trying to use Windows Authentication credentials to connect with my native (Winforms, console app) client to Identity Server hosted on IIS. The point is for user to be authenticated by AD and with those credentials get the right claims and roles from the Identity Server (which is run through commercial https://commercial.abp.io/ platform).
EDIT:
I found out it is not client related issue since i cannot use my External login (Windows credentials) even directly on hosted site.
The thing worked locally while hosted by IISExpress, then i published it to IIS and enabled the Anonymous and Windows Authentication in the IIS settings and here is where problems began.
When i run it and click the External Login (Windows Credentials) button i usually get a redirect to https://myserver/Error?httpStatusCode=401
and i get prompt for my windows credentials (which even if i insert correctly, just repeat prompt again).
From time to time i get logged in with my Windows credentials (which is the goal). Login with username and password works fine.
I saw the similar issue mentioned by someone here:
https://github.com/IdentityServer/IdentityServer4/issues/4937 without any solution\answer.
My client is basically the sample NativeConsolePKCEClient from this https://github.com/damienbod/AspNetCoreWindowsAuth
static string _authority = "https://myserver/";
string redirectUri = "https://127.0.0.1:45656";
var options = new OidcClientOptions
{
Authority = _authority,
ClientId = "native.code",
ClientSecret = "secret",
RedirectUri = redirectUri,
Scope = "openid profile",
FilterClaims = false,
Browser = browser,
Flow = OidcClientOptions.AuthenticationFlow.AuthorizationCode,
ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect,
LoadProfile = true
};
_oidcClient = new OidcClient(options);
var result = await _oidcClient.LoginAsync();
and on server side the startup configuration services:
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.Configure<IISOptions>(iis => // IISOptions
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
context.Services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); ;
options.Audience = "ABPIdentityServer";
});
}
Here is the ProcessWindowsLoginAsync challenge method:
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync(Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme);
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, tresting windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = "./ExternalLoginCallback",
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme },
}
};
var id = new ClaimsIdentity(Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
// add the groups as claims -- be careful if the number of groups is too large
{
var wi = (WindowsIdentity)wp.Identity;
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props);
return Redirect(props.RedirectUri);
}
else
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge("Windows");
}
}
I am suspecting that this piece of code when calling Challenge somehow returns up redirecting to error page, but i am not sure and i do now why.
So what am i missing? Is it even possible to run both Windows and Anonymous authentication on IIS?
Here i also found similar issue:
identity server 4 windows authentication
but the presented answers did not help me.
I strongly suspect that it's not the client issue it's the token provider's issue (Not the ID4 library but one where you have installed the ID4 library).
I believe that you have added the below code in the AccountController->Login action but make sure that you have added a success check in it, if you miss that then your app will go infinite loop.
[HttpGet] public async Task<IActionResult> Login(string returnUrl)
{
if(loginViewModel.ExternalLoginScheme == "Windows")
{
var authenticationResult = await HttpContext.AuthenticateAsync("Windows").ConfigureAwait(false);
if (authenticationResult.Succeeded && authenticationResult?.Principal is WindowsPrincipal windowsPrinciple)
{
// Add your custom code here
var authProps = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", "Windows"},
}
};
await HttpContext.SignInAsync();
return Redirect(RedirectUri);
}
else
{
return Challenge("Windows");
}
}
}
I hope this will help you to fix your issue.
Happy Coding!!
Just for anybody who might be interested. I found out what was causing the redirect error.
It is somehow connected with the ABP Suite i used for generating the base application.
there in the ApplicationInitialization there was a middleware called
app.UseErrorPage();
Which when the Windows credentials were challenged took it as an Error and redirected to https://myserver/Error?httpStatusCode=401.
I am not sure how this middleware works and why sometimes login worked, but removing this part solved my issue.
I hope this helps somebody, somehow, sometime..

How to validate if user exist inside IdentityServer4 after being authenticated from External Provider?

I'm trying to find a proper way where I can inject a service to validate if user exists or registered in my application after being successfully authenticated from an external identity provider like Azure Active Directory. What I want to do is to redirect user to a custom error page or display an Unauthorized message if his account is not yet registered in my application.
I tried utilizing the IProfileService interface but it seems not the right way to go.
Here is my Startup.cs setup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddTestUsers(Config.GetUsers())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients()) // Client was configured with RequireConsent = false, EnableLocalLogin = false,
.AddProfileService<ProfileService>()
.Services.AddTransient<IUserRepository,UserRepository>();
services.AddAuthentication()
.AddOpenIdConnect("AAD", "Azure Active Directory", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://login.microsoftonline.com/MyTenant";
options.ClientId = "MyClientId";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
options.GetClaimsFromUserInfoEndpoint = true;
});
}
public class ProfileService : IProfileService
{
private readonly IUserRepository _userRepository;
public ProfileService(IUserRepository userRepository)
{
_userRepository = userRepository
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = _userRepository.FindByUser(context.Subject.Identity.Name);
// This will display HTTP 500 instead of 401
if(user == null) throw new UnauthorizedAccessException("You're not registered");
// I add custom claims here
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context) => Task.FromResult(0);
}
Is there any available service or interface I can use where I can inject my user validation as well as allowing me to inject my user repository in that service? Is it possible to inject this kind of process inside IdentityServer4? Can someone point me in the right direction to accomplish my goal using IdentityServer4?
Note: Lets assume I have SPA web app and I have my own separate registration mechanism. I don't want to redirect back to my SPA if user doesn't exist and handle it inside IdentityServer4 instead. Btw, some of the code above are not included for brevity.
The IdentityServer4 QuickStart UI is configured to auto-provision local user accounts when signing-in through an external provider. That's all handled in ExternalController.Callback:
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = AutoProvisionUser(provider, providerUserId, claims);
}
In your situation, you can perform whatever logic you need to perform instead of calling AutoProvisionUser. As this is a regular MVC action that's being executed, you have the ability to inject your own classes into ExternalController's constructor or into Callback itself (using [FromServices]). Here's a rough idea of the changes you might want to make:
public async Task<IActionResult> Callback([FromServices] IUserRepository userRepository)
{
...
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
if (user == null)
{
// We don't have a local user.
return RedirectToAction("SomeAction", "SomeController");
}
...
}
You can write your custom logic in ExternalLoginCallback function in in AccountController if you are using ASP.NET Identity . After getting JWT token issued from Azure AD , you can decode the token ,get the user claims such as email/name :
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
// read external identity from the temporary cookie
var aadResult1 = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (aadResult1?.Succeeded != true)
{
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var externalUser = aadResult1.Principal;
if (externalUser == null)
{
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var claims = externalUser.Claims.ToList();
// try to determine the unique id of the external user - the most common claim type for that are the sub claim and the NameIdentifier
// depending on the external provider, some other claim type might be used
var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
if (userIdClaim == null)
{
userIdClaim = claims.FirstOrDefault(x => x.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier");
}
if (userIdClaim == null)
{
throw new Exception("Unknown userid");
}
Then you can write your service implement/logic in database to confirm whether user is already in database . If yes , login in user;if no , redirect user to confirmation/register view . Something like:
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync("YourProvider", userIdClaim.Value, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in with {Name} provider.", "YourProvider");
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToAction(nameof(Lockout));
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = "YourProvider";
var email = claims.FirstOrDefault(x => x.Type == ClaimTypes.Upn).Value;
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}
It depends on you for how to link AD user to local database user .use Azure AD's object ID or UPN .

How to get user id from IdentityServer in client app when including access token?

I have implemented an authentication service based on IdentityServer3 and a simple MVC client app and a Shopper API secured by the authentication service. I've implemented a IdentityServer custom UserService so that the authentication service authenticates against our existing user data store. My Shopper API expects a userid in the Shopper Get request. Currently the response from authentication service includes the identity token and the access token, but no user id. I tried adding a user_id claim in the AuthenticationResult from my custom UserService.AuthenticateLocalAsync method, but I'm not seeing it in my client app code.
UserService.AuthenticateLocalAsync looks like this:
try
{
var user = new shopper(_dbConnLib, context.UserName, context.Password);
var claims = new List<Claim> { new Claim("user_id", user.shopperid) };
context.AuthenticateResult = new AuthenticateResult(user.shopperid, user.MemberDetail.billToAddress.FirstName, claims);
}
catch(shopperInitFromException ex)
{
context.AuthenticateResult = null; // Indicates username/password failure
}
return Task.FromResult(0);
And my client app SecurityTokenValidated handler looks like this:
SecurityTokenValidated = async n =>
{
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo").ToString());
var userInfo = await userInfoClient.GetAsync(n.ProtocolMessage.AccessToken);
userInfo.Claims.ToList().ForEach(ui => nid.AddClaim(new Claim(ui.Type, ui.Value)));
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
//nid.AddClaim(new Claim("user_id", n.ProtocolMessage.UserId));
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
If I step through that in the debugger, userInfo.Claims always has a count of 0. How can I get back a claim with the unique identifier of the user? Or can I get it from the identity or access token? Or should I just pass the tokens to the Shopper API and let it determine the id from the tokens?
I think I may have the answer. So far, as far as I can tell, the claims I include in the AuthenticateResult constructor in my override of AuthenticateLocalAsync don't seem to go anywhere. But the claims I include in my override of GetProfileDataAsync appear in the token. My GetProfileDataAsync code, which appears to set the claims properly, looks like this:
public override Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = new shopper(_dbConnLib, context.Subject.FindFirst("sub").Value);
var claims = new List<Claim> { new Claim("sub", user.shopperid), new Claim("acr_level", "level 0"), new Claim("amr", "anonymous") };
context.IssuedClaims = claims;
return Task.FromResult(0);
}
My AuthenticateLocalAsync code that sets claims in the AuthenticateResult that I never see in my client app code looks like this:
public override Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
// TODO: Handle AddshopperToBasketException in UserService.AuthenticateLocalAsync
try
{
var user = new shopper(_dbConnLib, context.UserName, context.Password);
var claims = new List<Claim> { new Claim("acr_level", "level 0"), new Claim("amr", "anonymous") };
context.AuthenticateResult = new AuthenticateResult(user.shopperid, user.MemberDetail.billToAddress.FirstName, claims);
}
catch(shopperInitFromException ex)
{
context.AuthenticateResult = null; // Indicates username/password failure
}
return Task.FromResult(0);
}

Categories