Related
I'm trying to validate a token that was generated in Azure Active Directory.
But the result of the conversion is an error. I've tried changing several settings, but to no avail.
I'm implementing the test in a Console Application.
What could be wrong?
Error
IDX10511: Signature validation failed. Keys tried: 'Microsoft.IdentityModel.Tokens.X509SecurityKey, KeyId: '2ZQpJ3UpbjAYXYGaXEJl8lV0TOI', InternalId: '2ZQpJ3UpbjAYXYGaXEJl8lV0TOI'. , KeyId: 2ZQpJ3UpbjAYXYGaXEJl8lV0TOI
'.
Number of keys in TokenValidationParameters: '14'.
Number of keys in Configuration: '0'.
Matched key was in 'TokenValidationParameters'.
kid: '2ZQpJ3UpbjAYXYGaXEJl8lV0TOI'.
Exceptions caught:
''.
token: '{"typ":"JWT","nonce":"Dl7E6r5PvOq2hGS909Qdgz6KiLjXBsLUS_XhofbRH1k","alg":"RS256","x5t":"2ZQpJ3UpbjAYXYGaXEJl8lV0TOI","kid":"2ZQpJ3UpbjAYXYGaXEJl8lV0TOI"}.{"aud":"https://graph.microsoft.com","iss":"https://sts.windows.net/f3211d0e-125b-42c3-86db-322b19a65a22/","iat":1664377202,"nbf":1664377202,"exp":1664381102,"aio":"E2ZgYLD1ms4yn3nR69oIsXXzJ/DoAwA=","app_displayname":"66943_Portal_Claro_NDI_PRD_Staging_ESOPortal","appid":"8bd88acb-22ff-4698-9e35-eb877d48e837","appidacr":"1","idp":"https://sts.windows.net/f3211d0e-125b-42c3-86db-322b19a65a22/","idtyp":"app","oid":"d7ff5f9f-86f3-4e6d-9203-33b820b90e73","rh":"0.ASYADh0h81sSw0KG2zIrGaZaIgMAAAAAAAAAwAAAAAAAAAAmAAA.","sub":"d7ff5f9f-86f3-4e6d-9203-33b820b90e73","tenant_region_scope":"NA","tid":"f3211d0e-125b-42c3-86db-322b19a65a22","uti":"3bt0MgzfykqBvr1x789_AA","ver":"1.0","wids":["0997a1d0-0d1d-4acb-b408-d5ca73121e90"],"xms_tcdt":1403205942}'.
Token JWT
eyJ0eXAiOiJKV1QiLCJub25jZSI6IkRsN0U2cjVQdk9xMmhHUzkwOVFkZ3o2S2lMalhCc0xVU19YaG9mYlJIMWsiLCJhbGciOiJSUzI1NiIsIng1dCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGxWMFRPSSIsImtpZCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGxWMFRPSSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9mMzIxMWQwZS0xMjViLTQyYzMtODZkYi0zMjJiMTlhNjVhMjIvIiwiaWF0IjoxNjY0Mzc3MjAyLCJuYmYiOjE2NjQzNzcyMDIsImV4cCI6MTY2NDM4MTEwMiwiYWlvIjoiRTJaZ1lMRDFtczR5bjNuUjY5b0lzWFh6Si9Eb0F3QT0iLCJhcHBfZGlzcGxheW5hbWUiOiI2Njk0M19Qb3J0YWxfQ2xhcm9fTkRJX1BSRF9TdGFnaW5nX0VTT1BvcnRhbCIsImFwcGlkIjoiOGJkODhhY2ItMjJmZi00Njk4LTllMzUtZWI4NzdkNDhlODM3IiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvZjMyMTFkMGUtMTI1Yi00MmMzLTg2ZGItMzIyYjE5YTY1YTIyLyIsImlkdHlwIjoiYXBwIiwib2lkIjoiZDdmZjVmOWYtODZmMy00ZTZkLTkyMDMtMzNiODIwYjkwZTczIiwicmgiOiIwLkFTWUFEaDBoODFzU3cwS0cyeklyR2FaYUlnTUFBQUFBQUFBQXdBQUFBQUFBQUFBbUFBQS4iLCJzdWIiOiJkN2ZmNWY5Zi04NmYzLTRlNmQtOTIwMy0zM2I4MjBiOTBlNzMiLCJ0ZW5hbnRfcmVnaW9uX3Njb3BlIjoiTkEiLCJ0aWQiOiJmMzIxMWQwZS0xMjViLTQyYzMtODZkYi0zMjJiMTlhNjVhMjIiLCJ1dGkiOiIzYnQwTWd6ZnlrcUJ2cjF4Nzg5X0FBIiwidmVyIjoiMS4wIiwid2lkcyI6WyIwOTk3YTFkMC0wZDFkLTRhY2ItYjQwOC1kNWNhNzMxMjFlOTAiXSwieG1zX3RjZHQiOjE0MDMyMDU5NDJ9.uIBkYSdbR_GcnI-QCM0FI-TJfu5Q5HotlWDB8ee-UqAL-7j8Sg2AqFflwfsF9Qsu4qlsZU7ymISY_SEzWxrsTpAxVYvCdNfuki7tm1WTyhiN_fDZWKM9VGTUrXxFNd5FQL7cAMkq6JfjO13fRs4R7ZOTlWtWS0DCOrZ2Cy506he1Ip0AXAGJFwLKD3aZ8-6ZIV5hHGoluUwE78OSWHCMVXzaliYLhfiYPmGSFqP6OK3AaQysFjEbN_54zh9jy9GrEvGQHCYXN0sueDao2n77qkagRrC67W0pkJHvFLCqTypy1FJfhhpZyKtzLbqC1tmOLPZv_a0_sgJY1Z245l-pug
My Code
private static JwtSecurityToken ValidateAzureToken(string token)
{
string _tenantId = ConfigurationManager.AppSettings["tenantId"];
string _clientId = ConfigurationManager.AppSettings["clientId"];
string _authority = ConfigurationManager.AppSettings["authority"];
string _audience = ConfigurationManager.AppSettings["audience"];
string authority = string.Format(CultureInfo.InvariantCulture, _authority, _tenantId);
ConfigurationManager<OpenIdConnectConfiguration> _configManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{authority}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration config = _configManager.GetConfigurationAsync().Result;
IList<string> validissuers = new List<string>()
{
$"https://login.microsoftonline.com/{_tenantId}/",
$"https://login.microsoftonline.com/{_tenantId}/v2.0",
$"https://login.windows.net/{_tenantId}/",
$"https://login.microsoft.com/{_tenantId}/",
$"https://sts.windows.net/{_tenantId}/"
};
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidAudiences = new[] { _audience, _clientId },
ValidIssuers = validissuers,
IssuerSigningKeys = config.SigningKeys
};
IdentityModelEventSource.ShowPII = true;
SecurityToken securityToken;
try
{
var claims = new JwtSecurityTokenHandler().ValidateToken(token, validationParameters, out securityToken);
}
catch (Exception e)
{
return null;
}
return securityToken as JwtSecurityToken;
}
I'm using OWIN 4.2 with .NET Framework 4.7.2 for my ASP.NET MVC client app.
Login works completely fine but logout will fail.
On my client's startup.cs
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = "MVC",
ClientSecret = "MVC-Secret",
Authority = "https://localhost:44305/",
RedirectUri = "https://localhost:44347/",
CallbackPath = new PathString("/"),
Scope = "openid api",
SignInAsAuthenticationType = "cookie",
RequireHttpsMetadata = false,
UseTokenLifetime = false,
RedeemCode = true,
SaveTokens = true,
ResponseType = OpenIdConnectResponseType.Code,
ResponseMode = OpenIdConnectResponseMode.Query,
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to the OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
// generate code verifier and code challenge
var codeVerifier = CryptoRandom.CreateUniqueId(32);
string codeChallenge;
using (var sha256 = SHA256.Create())
{
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
codeChallenge = Base64Url.Encode(challengeBytes);
}
// set code_challenge parameter on authorization request
n.ProtocolMessage.SetParameter("code_challenge", codeChallenge);
n.ProtocolMessage.SetParameter("code_challenge_method", "S256");
// remember code verifier in cookie (adapted from OWIN nonce cookie)
// see: https://github.com/scottbrady91/Blog-Example-Classes/blob/master/AspNetFrameworkPkce/ScottBrady91.BlogExampleCode.AspNetPkce/Startup.cs#L85
RememberCodeVerifier(n, codeVerifier);
}
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token").Value;
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
}
return Task.CompletedTask;
},
AuthorizationCodeReceived = n =>
{
// get code verifier from cookie
// see: https://github.com/scottbrady91/Blog-Example-Classes/blob/master/AspNetFrameworkPkce/ScottBrady91.BlogExampleCode.AspNetPkce/Startup.cs#L102
var codeVerifier = RetrieveCodeVerifier(n);
// attach code_verifier on token request
n.TokenEndpointRequest.SetParameter("code_verifier", codeVerifier);
return Task.CompletedTask;
},
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(
id,
n.AuthenticationTicket.Properties);
return Task.FromResult(0);
},
}
}
);
I also tried
...
Authority = "https://localhost:44305/",
RedirectUri = "https://localhost:44347/",
PostLogoutRedirectUri = "https://localhost:44347/signout-callback-oidc",
...
And also
...
Authority = "https://localhost:44305/",
RedirectUri = "https://localhost:44347/",
PostLogoutRedirectUri = "https://localhost:44347/",
...
However, all these results in the response
error:invalid_request
error_description:The specified 'post_logout_redirect_uri' is invalid.
error_uri:https://documentation.openiddict.com/errors/ID2052
On my server, the configuration is as follows
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = clientId,
ClientSecret = clientSecret,
DisplayName = displayName,
RedirectUris =
{
new Uri("https://localhost:44347/")
},
Permissions =
{
...
},
PostLogoutRedirectUris =
{
new Uri("https://localhost:44347/")
}
}, cancellationToken);
}
I have also tried changing Server config to
PostLogoutRedirectUris =
{
new Uri("https://localhost:44347/signout-callback-oidc")
}
I encountered the same issue, what solved it for me was to add the logout permission in the application - OpenIddictConstants.Permissions.Endpoints.Logout
await _applicationManager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
DisplayName = "MVC client application",
PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") },
RedirectUris = { new Uri("http://localhost:53507/signin-oidc") },
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Logout,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode
}
});
As answered by Apps in here https://stackoverflow.com/a/69671657/6477254, I can confirm that you must allow permission for the logout endpoint, using the constant value of OpenIddictConstants.Permissions.Endpoints.Logout, which hold "ept:logout" string value when creating the OpenIddict data.
I've read the docs and followed the examples but I am unable to get user claims into the access token. My client is not ASP.NET core, so the configuration of the MVC client is not the same as the v4 samples.
Unless I have misunderstood the docs, the ApiResources are used to populate the RequestedClaimTypes in the profile service when creating the access token. The client should add the api resource to it's list of scopes to include associated userclaims. In my case they are not being connected.
When ProfileService.GetProfileDataAsync is called with a caller of "ClaimsProviderAccessToken", the requested claim types are empty. Even if I set the context.IssuedClaims in here, when it is called again for "AccessTokenValidation" the claims on the context are not set.
In the MVC app:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
UseTokenLifetime = false,
ClientId = "portal",
ClientSecret = "secret",
Authority = authority,
RequireHttpsMetadata = false,
RedirectUri = redirectUri,
PostLogoutRedirectUri = postLogoutRedirectUri,
ResponseType = "code id_token",
Scope = "openid offline_access portal",
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
await AssembleUserClaims(n);
},
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
});
private static async Task AssembleUserClaims(AuthorizationCodeReceivedNotification notification)
{
string authCode = notification.ProtocolMessage.Code;
string redirectUri = "https://myuri.com";
var tokenClient = new TokenClient(tokenendpoint, "portal", "secret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(authCode, redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(new Uri(userinfoendpoint), tokenResponse.AccessToken);
var userInfoResponse = await userInfoClient.GetAsync();
// create new identity
var id = new ClaimsIdentity(notification.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", notification.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", notification.AuthenticationTicket.Identity.FindFirst("sid").Value));
notification.AuthenticationTicket = new AuthenticationTicket(id, notification.AuthenticationTicket.Properties);
}
Identity Server Client:
private Client CreatePortalClient(Guid tenantId)
{
Client portal = new Client();
portal.ClientName = "Portal MVC";
portal.ClientId = "portal";
portal.ClientSecrets = new List<Secret> { new Secret("secret".Sha256()) };
portal.AllowedGrantTypes = GrantTypes.HybridAndClientCredentials;
portal.RequireConsent = false;
portal.RedirectUris = new List<string> {
"https://myuri.com",
};
portal.AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"portal"
};
portal.Enabled = true;
portal.AllowOfflineAccess = true;
portal.AlwaysSendClientClaims = true;
portal.AllowAccessTokensViaBrowser = true;
return portal;
}
The API resource:
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource
{
Name= "portalresource",
UserClaims = { "tenantId","userId","user" },
Scopes =
{
new Scope()
{
Name = "portalscope",
UserClaims = { "tenantId","userId","user",ClaimTypes.Role, ClaimTypes.Name),
},
}
},
};
}
The Identity resource:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
// some standard scopes from the OIDC spec
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource("portal", new List<string>{ "tenantId", "userId", "user", "role", "name"})
};
}
UPDATE:
Here is the interaction between the MVC app and the Identity Server (IS):
MVC:
Owin Authentication Challenge
IS:
AccountController.LoginAsync - assemble user claims and call HttpContext.SignInAsync with username and claims)
ProfileService.IsActiveAsync - Context = "AuthorizeEndpoint", context.Subject.Claims = all userclaims
ClaimsService.GetIdentityTokenClaimsAsync - Subject.Claims (all userclaims), resources = 1 IdentityResource (OpenId), GrantType = Hybrid
MVC:
SecurityTokenValidated (Notification Callback)
AuthorizationCodeReceived - Protocol.Message has Code and IdToken call to TokenClient.RequestAuthorizationCodeAsync()
IS:
ProfileService.IsActiveAsync - Context = "AuthorizationCodeValidation", context.Subject.Claims = all userclaims
ClaimsService.GetAccessTokenClaimsAsync - Subject.Claims (all userclaims), resources = 2 IdentityResource (openId,profile), GrantType = Hybrid
ProfileService.GetProfileDataAsync - Context = "ClaimsProviderAccessToken", context.Subject.Claims = all userclaims, context.RequestedClaimTypes = empty, context.IssuedClaims = name,role,user,userid,tenantid
ClaimsService.GetIdentityTokenClaimsAsync - Subject.Claims (all userclaims), resources = 2 IdentityResource (openId,profile), GrantType = authorization_code
MVC:
call to UserInfoClient with tokenResponse.AccessToken
IS:
ProfileService.IsActiveAsync - Context = "AccessTokenValidation", context.Subject.Claims = sub,client_id,aud,scope etc (expecting user and tenantId here)
ProfileService.IsActiveAsync - Context = "UserInfoRequestValidation", context.Subject.Claims = sub,auth_time,idp, amr
ProfileService.GetProfileDataAsync - Context = "UserInfoEndpoint", context.Subject.Claims = sub,auth_time,idp,amp, context.RequestedClaimTypes = sub
As I'm not seeing what happens in your await AssembleUserClaims(context); I would suggest to check if it is doing the following:
Based on the the access token that you have from either the context.ProtoclMessage.AccessToken or from the call to the TokenEndpoint you should create a new ClaimsIdentity. Are you doing this, because you are not mentioning it?
Something like this:
var tokenClient = new TokenClient(
IdentityServerTokenEndpoint,
"clientId",
"clientSecret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaims(n.AuthenticationTicket.Identity.Claims);
// get user info claims and add them to the identity
var userInfoClient = new UserInfoClient(IdentityServerUserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var userInfoEndpointClaims = userInfoResponse.Claims;
// this line prevents claims duplication and also depends on the IdentityModel library version. It is a bit different for >v2.0
id.AddClaims(userInfoEndpointClaims.Where(c => id.Claims.Any(idc => idc.Type == c.Type && idc.Value == c.Value) == false));
// create the authentication ticket
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
And one more thing - read this regarding the resources. In your particular case, you care about IdentityResources (but I see that you also have it there).
So - when calling the UserInfoEndpoint do you see the claims in the response? If no - then the problem is that they are not issued.
Check these, and we can dig in more.
Good luck
EDIT
I have a solution that you may, or may not like, but I'll suggest it.
In the IdentityServer project, in the AccountController.cs there is a method public async Task<IActionResult> Login(LoginInputModel model, string button).
This is the method after the user has clicked the login button on the login page (or whatever custom page you have there).
In this method there is a call await HttpContext.SignInAsync. This call accept parameters the user subject, username, authentication properties and list of claims. Here you can add your custom claim, and then it will appear when you call the userinfo endpoint in the AuthorizationCodeReceived. I just tested this and it works.
Actually I figured out that this is the way to add custom claims. Otherwise - IdentityServer doesn't know about your custom claims, and is not able to populate them with values. Try it out and see if it works for you.
You need to modify the code of "Notifications" block in MVC App like mentioned below:
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n => {
var userInfoClient = new UserInfoClient(UserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(n.ProtocolMessage.AccessToken);
var identity = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
identity.AddClaims(userInfoResponse.Claims);
var tokenClient = new TokenClient(TokenEndpoint, "portal", "secret");
var response = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
identity.AddClaim(new Claim("access_token", response.AccessToken));
identity.AddClaim(new Claim("expires_at", DateTime.UtcNow.AddSeconds(response.ExpiresIn).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
identity.AddClaim(new Claim("refresh_token", response.RefreshToken));
identity.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(identity, n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token").Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
return Task.FromResult(0);
}
}
(consider if any changes related to the version of identity server as this code was built for identity server 3.)
Why do you have "portal" listed as an identity resource and Api resource? That could be causing some confusion.
Also, before I switched to IdentityServer4 and asp.net core, my IdentityServer3 startup code looked very similar to what you have with MVC. You may want to look at the examples for IdentityServer3.
Some suggestions I may give, in your "ResponseType" field for MVC, you could try "code id_token token"
Also, you are setting your claims on AuthorizationCodeReceived, instead use SecurityTokenValidated.
But you shouldn't have to do anything custom like people are mentioning. IdentityServer4 handles custom ApiResources like you are attempting to do.
You can try to implement your own IProfileService and override it following way:
services.AddIdentityServer()
.//add clients, scopes,resources here
.AddProfileService<YourOwnProfileProvider>();
For more information look up here:
https://damienbod.com/2016/10/01/identityserver4-webapi-and-angular2-in-a-single-asp-net-core-project/
portal is not an identity resource: you should remove
new IdentityResource("portal", new List{ "tenantId",
"userId", "user", "role", "name"})
Names for the api resources should be consistent:
public static IEnumerable GetApiResources()
{
return new List
{
new ApiResource
{
Name= "portal",
UserClaims = { "tenantId","userId","user" },
Scopes =
{
new Scope("portal","portal")
}
},
};
}
Try setting GrantTypes.Implicit in the client.
I am using identity server 4 for authentication using grant type as 'ResourceOwnerPassword'. I am able to authenticate the user but not able to get claims related to user. So how can I get those ?
Below is my code
Client
Startup.cs
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5000",
RequireHttpsMetadata = false,
ApiName = "api1"
});
Controller
public async Task<IActionResult> Authentication(LoginViewModel model)
{
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(model.Email, model.Password, "api1");
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
}
// Here I am not getting the claims, it is coming Forbidden
var extraClaims = new UserInfoClient(disco.UserInfoEndpoint);
var identityClaims = await extraClaims.GetAsync(tokenResponse.AccessToken);
if (!tokenResponse.IsError)
{
Console.WriteLine(identityClaims.Json);
}
Console.WriteLine(tokenResponse.Json);
Console.WriteLine("\n\n");
}
Server
Startup.cs
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<IdentityProfileService>()
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>();
Config.cs
public static IEnumerable<Client> GetClients(IConfigurationRoot Configuration)
{
// client credentials client
return new List<Client>
{
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true,
AccessTokenType = AccessTokenType.Jwt
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
But when I check my access token in jwt.io there I can see the claims But why I am not able to get in the controller ?
Any help on this appreciated !
You can call the UserInfoEndpoint, as per your example, but you can also get additional claims if you define your ApiResource as requiring them.
For example, rather than just defining your ApiResource like you are:
new ApiResource("api1", "My API")
You can use the expanded format and define what UserClaims you'd like to have when getting an access token for this scope.
For example:
new ApiResource
{
Name = "api1",
ApiSecrets = { new Secret(*some secret*) },
UserClaims = {
JwtClaimTypes.Email,
JwtClaimTypes.PhoneNumber,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName,
JwtClaimTypes.PreferredUserName
},
Description = "My API",
DisplayName = "MyApi1",
Enabled = true,
Scopes = { new Scope("api1") }
}
Then in your own implementation of the IProfileService you will find that calls to GetProfileDataAsync have a list of what claims are requested in the context (ProfileDataRequestContext.RequestedClaimTypes). Given that list of what's been asked for, you can then add any claims you like - however you like - to the context.IssuedClaims that you return from that method. These will then be a part of the access token.
If you only want certain claims by specifically calling the UserInfo endpoint though, you'll want to create an IdentityResource definition and have that scope included as part of your original token request.
For example:
new IdentityResource
{
Name = "MyIdentityScope",
UserClaims = {
JwtClaimTypes.EmailVerified,
JwtClaimTypes.PhoneNumberVerified
}
}
But your first problem is following the other answer here so you don't get 'forbidden' as the response to the UserInfo endpoint!
Try sending the token along the request, when calling the UserInfoEndpoint. Try this:
var userInfoClient = new UserInfoClient(doc.UserInfoEndpoint, token);
var response = await userInfoClient.GetAsync();
var claims = response.Claims;
official docs
I have a bit of a Frankenstien service in that it has endpoints for both SOAP and REST hosted on the same URL by the same code base. I'm using the client credentials grant flow to successfully secure the REST endpoints, but would like to use the same process to secure the SOAP calls. The startup.cs initializes the Identity server bearer token authentication like so:
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["IdentityServerUrl"],
RequiredScopes = new[] { ConfigurationManager.AppSettings["IdentityServerScopes"] }
});
And for the REST endpoints I add an
[Authorize]
code decoration and everything works. For the SOAP side I repurposed the password field and have sent the token through that and can decode it like so:
string sPassword = request.Authentication.Password;
if (sPassword.Contains("."))
{
"\nAccess Token (decoded):".ConsoleGreen();
var parts = sPassword.Split('.');
var header = parts[0];
var claims = parts[1];
Console.WriteLine(JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(header))));
Console.WriteLine(JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(claims))));
}
I can see the claims but this isn't validating the token. From here I've pieced together a ValidateToken method that throws exceptions about the Signature validation failed. Unable to resolve SecurityKeyIdentifier. I'm fairly certain that everything has been signed by the IdentityServer3 cert, but I'm stuck trying to create a cert. I don't have any certs in my KeyStore and would like a solution that doesn't require that I insert the cert in the KeyStore. Here is the attempt:
public static bool VerifyToken(string token)
{
const string thumbPrint = "6bf8e136eb36d4a56ea05c7ae4b9a45b63bf975d"; // correct thumbprint of certificate
var cert = X509CertificateHelper.FindByThumbprint(StoreName.My, StoreLocation.LocalMachine, thumbPrint).First();
var validationParameters = new TokenValidationParameters()
{
//IssuerSigningToken = new BinarySecretSecurityToken(_key),
IssuerSigningToken = new X509SecurityToken(cert),
ValidAudience = "https://securityeli.twcable.com/core/resources",
ValidIssuer = "https://securityeli.twcable.com/core",
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true
//ValidateIssuerSigningKey = true
};
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken validatedToken = null;
try
{
tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
//... manual validations return false if anything untoward is discovered
return validatedToken != null;
}
public class X509CertificateHelper
{
public static IEnumerable<X509Certificate2> FindByThumbprint(StoreName storeName, StoreLocation storeLocation, string thumbprint)
{
var store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
foreach (var certificate in certificates)
{
yield return certificate;
}
store.Close();
}
}
The current process doesn't work because I have no keys in my keystore. The BinarySecretSecurityToken failed because I don't know the key length?
I'm also going to come back to the REST side of the house, it validates the bearer token using the Authorize tag, so I should have access to the cert but have no idea how to get it out of the application. I can see in Startup it get passed IAPPBuilder app that I haven't been able to access.
Two questions are how to I create a cert to validate a token created in IdentityServer3 in C#? And can I retrieve that cert somehow?
After trying multiple paths I finally found something that works, I'm going to try and capture the relevant parts in case someone else is ever trying to do the same thing.
First I split the incoming token into it's parts:
var parts = sPassword.Split('.');
var header = parts[0];
var claims = parts[1];
var token = new JwtSecurityToken(sPassword);
I then setup some variables and called a custom VerifyToken method:
CustomResponse customResponse = null;
SecurityToken validatedToken = null;
ClaimsPrincipal claimsPrincipal = null;
if (VerifyToken(sPassword, ref customResponse, ref validatedToken, ref claimsPrincipal))
{
// Process SOAP request after authentication
}
else
return customResponse; // token wasn't authenticated, and not authorized message was set in the VerifyToken method
The VerifyToken method looks like this:
public static bool VerifyToken(string token, ref CustomResponse customResponse, ref SecurityToken validatedToken, ref ClaimsPrincipal claimsPrincipal)
{
// This was the biggest challenge in finding the cert that is used to validate the token
var certString = "Found in the CallbackController.cs in the IdentityServer3.Samples repository"
var cert = new X509Certificate2(Convert.FromBase64String(certString));
// Setting what you'd like the authorization to validate.
var validationParameters = new TokenValidationParameters()
{
IssuerSigningToken = new X509SecurityToken(cert),
ValidAudience = ConfigurationManager.AppSettings["IdentityServerUrl"] + "/resources",
ValidIssuer = ConfigurationManager.AppSettings["IdentityServerUrl"],
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true
};
var tokenHandler = new JwtSecurityTokenHandler();
try
{
claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
}
catch (SecurityTokenValidationException e)
{
//HttpContext.Current.Response.StatusCode = 401;
//statusCode = HttpStatusCode.Unauthorized;
customResponse = new CustomResponse();
customResponse.ServiceReturnStatus = new ServiceReturnStatus();
customResponse.ServiceReturnStatus.ReturnCode = -401;
customResponse.ServiceReturnStatus.ReturnMessage = "Unauthorized";
}
catch (Exception e)
{
//HttpContext.Current.Response.StatusCode = 403;
//statusCode = HttpStatusCode.InternalServerError;
customResponse = new CustomResponse();
customResponse.ServiceReturnStatus = new ServiceReturnStatus();
customResponse.ServiceReturnStatus.ReturnCode = -403;
customResponse.ServiceReturnStatus.ReturnMessage = "Internal Server Error";
}
//... manual validations return false if anything untoward is discovered
return validatedToken != null;
}
private string GetClaimFromPrincipal(ClaimsPrincipal principal, string claimType)
{
var uidClaim = principal != null && principal.Claims != null ? principal.Claims.FirstOrDefault(s => s.Type == claimType) : null;
return uidClaim != null ? uidClaim.Value : null;
}
I also added a GetClaimFromPrincipal that you can use to get claims out of the principal.
That's it, it doesn't look all that complicated, but it sure took me a lot of trial and error to get it to work. I'd still like an option that uses the Owin Startup information to validate/authorize the token because all I did was basically load all the information that I loaded in the Startup.cs like so:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["IdentityServerUrl"],
RequiredScopes = new[] { ConfigurationManager.AppSettings["IdentityServerScopes"] }
});