IdentityServer4 ADFS External Not Returning Roles - asp.net-core

I have created an IdentityServer4 IDP using the standard template for Core Identity. I am looking to have an External provider being out ADFS 2016 Server. I have added this to the AddAuthentication() in Startup.cs
services.AddAuthentication()
.AddOpenIdConnect("adfs", "ADFS", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "[AuthURL]";
options.ClientId = "[ClientId]";
options.ResponseType = "id_token token code";
options.Scope.Add("profile");
options.Scope.Add("email");
options.CallbackPath = "/signin-adfs";
options.SignedOutCallbackPath = "/signout-callback-adfs";
options.RemoteSignOutPath = "/signout-adfs";
options.ClaimActions.Add(new JsonKeyClaimAction("role", null, "role"));
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
It successfully redirects to the ADFS login window. Once signed in it calls the ExternalController.cs CallBack() correctly and I do have a successful authentication.
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
The issue I am having is that I am not getting back the full user. I see claims but I do not see roles. I am used to seeing a JWT token which includes a list of Roles, however, I do not see these roles within the Result from above.
How can I either get a JWT token back from the Authentication against ADFS or have the roles returned and be within the Result?

Inside OIDC add scope for roles:
options.Scope.Add("roles");
Inside config.cs in IDS4 make sure your client has the allowed scopes:
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.OfflineAccess,
"roles",
},
If it's an API, make sure config.cs has the correct userclaims:
new ApiResource()
{
Name = "API",
DisplayName = "display",
ApiSecrets =
{
new Secret("secret".Sha256()),
new Secret
{
Value = "4A04D56554F731CCD123BB574D6918C8C83BDF65",
Type = "X509Thumbprint",
Description = "Certificate"
}
},
Scopes = new List<string>(){"postman Web API"},
Enabled = true,
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Email,
JwtClaimTypes.Subject,
JwtClaimTypes.Role,
JwtClaimTypes.Address,
JwtClaimTypes.Confirmation,
JwtClaimTypes.EmailVerified,
JwtClaimTypes.Id,
JwtClaimTypes.Profile
}
},

Related

nopCommerce Authentication using Identity Server4 Implementation Issue

I am using NopCommerce 4.40.3 and Identity Server 4. I am trying to integrate Identity Server authentication with NopCommerce. I have followed the existing ExternalAuth.Facebook project approach and created the plugin. I am able to install and configure the plugin successfully. I am able to show the "Identity Server Authentication" button and when I click it is redirecting to Identity Server Login Page and I am able to authenticate to Identity Server with out any issues. I can able to see the claims and I can see the success message in the identity server console. But NopCommerce site still shows not logged in and when I try to access different pages in NopCommerce, It is redirecting me to login page.
After login, I am able to see the Identity Server cookies like "idsrv" and "idsrv.session". When I logout from Identity Server, I can see both cookies are cleared out.
My question is, how can I set successful login to NopCommerce site. Here's my implementation code. I don't know what I have missed in the configuration or in the implementation. Please help me.
Identity Server Client Configuration:
new Client
{
ClientName = "MiniApple.App.NopCommerce",
ClientId = "MiniApple.App.NopCommerce",
AllowedGrantTypes =GrantTypes.HybridAndClientCredentials,
RedirectUris = new List<string>{ "https://localhost:44369/signin-oidc" }, //Client Application Address
RequirePkce = false,
RequireConsent = true,
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Address,
IdentityServerConstants.StandardScopes.Email,
"MiniApple.API.Employee",
"roles"},
ClientSecrets = { new Secret("abcdefghijklmnopqrstuvwxyz".Sha512()) },
AllowAccessTokensViaBrowser = true,
AlwaysSendClientClaims = true,
PostLogoutRedirectUris = new List<string> { "https://localhost:44369/signout-callback-oidc" }
},
NopCommerce Authentication Registration:
public class IdentityServerAuthenticationRegistrar : IExternalAuthenticationRegistrar
{
/// <summary>
/// Configure
/// </summary>
/// <param name="builder">Authentication builder</param>
public void Configure(AuthenticationBuilder builder)
{
builder.AddOpenIdConnect("oidc", options => {
var settings = EngineContext.Current.Resolve<IdentityServerExternalAuthSettings>();
options.SignInScheme = "Cookies";
options.Authority = settings.Authority;
options.ClientId = settings.ClientKeyIdentifier;
options.ResponseType = settings.ResponseType;
options.SaveTokens = true;
options.ClientSecret = settings.ClientSecret;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add(settings.Scope);
options.Scope.Add("roles");
options.ClaimActions.MapUniqueJsonKey("role", "role");
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "role"
};
});
}
}
Identity Server Console Output:
info: IdentityServer4.Validation.TokenRequestValidator[0]
Token request validation success, {
"ClientId": "MiniApple.App.NopCommerce",
"ClientName": "MiniApple.App.NopCommerce",
"GrantType": "authorization_code",
"AuthorizationCode": "****F988",
"RefreshToken": "********",
"Raw": {
"client_id": "MiniApple.App.NopCommerce",
"client_secret": "***REDACTED***",
"code": "18978F1D183EDFA3E3F5918B85F43DDFEAFE74D49E207E2449F59A9490BFF988",
"grant_type": "authorization_code",
"redirect_uri": "https://localhost:44369/signin-oidc"
}
}
info: IdentityServer4.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.UserInfoEndpoint for /connect/userinfo
info: IdentityServer4.ResponseHandling.UserInfoResponseGenerator[0]
Profile service returned the following claim types: given_name family_name role
After successful login with identity server, it is redirecting to https://localhost:44369/signin-oidc and I am getting 404 error.
The issue is similar to the below
[question]: https://www.nopcommerce.com/en/boards/topic/60547/problems-making-custom-externalauth-plugin-for-openidconnect-to-auth0
Once I have changed the code as below, the authentication is works fine.
public void Configure(AuthenticationBuilder builder)
{
builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => {
//Set Open Id Parameters
var settings = EngineContext.Current.Resolve<IdentityServerExternalAuthSettings>();
options.Authority = settings.Authority;
options.ClientId = settings.ClientId;
options.ClientSecret = settings.ClientSecret;
options.ResponseType = settings.ResponseType;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add(settings.Scope);
options.Scope.Add("roles");
options.Scope.Add("openid");
options.Scope.Add("profile");
//options.Scope.Add("email");
options.SaveTokens = true;
options.ClaimActions.MapUniqueJsonKey("role", "role");
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "role"
};
options.Events = new OpenIdConnectEvents
{
OnRemoteFailure = context =>
{
context.HandleResponse();
var errorUrl = context.Properties.GetString(IdentityServerAuthenticationDefaults.ErrorCallback);
context.Response.Redirect(errorUrl);
return Task.FromResult(0);
}
};
});
}

identityserver4 Hybrid follow without competed login

I have an API that needs to secure allowing 3rd party asp.net core MVC web application to request an access token and use this access token to request the secured API.
i created HybridAndClientCredentials client on the identity server
ClientId = "testclient",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
AllowOfflineAccess = true,
// secret for authentication
ClientSecrets =
{
new Secret("password".Sha256())
},
RedirectUris = {"http://127.0.0.1:55950",
"http://localhost/testLogin",
"https://localhost:44322/",
"https://localhost:44302/",
"https://localhost:44303/signin-oidc"},
RequireConsent = false,
// scopes that client has access to
AllowedScopes = { "roles" , IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile}
and MVC client as showing in identity server 4 documentation
.AddOpenIdConnect("oidc", options =>
{
options.Authority = Constants.Authority;
options.RequireHttpsMetadata = false;
options.ClientSecret = "password";
options.ClientId = "testclient";
options.ResponseType = "code id_token";
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
//options.Scope.Add("email");
//options.Scope.Add("resource1.scope1");
options.Scope.Add("offline_access");
options.ClaimActions.MapAllExcept("iss", "nbf", "exp", "aud", "nonce", "iat", "c_hash");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role,
};
});
it's working but the user appears to be logged in the MVC application all I want is the access token to be used in calling the secured API also I don't want to use [authorize] attribute on the MVC client to redirect the user to identity server login page.
Something in the MVC application must to trigger a challenge to the OpenIDconnect handler, that starts the process to authenticate the user. As a result the user is logged in and you get access to the tokens.
Using a [Authorize] attribute is one way to trigger a challenge, or to do it manually using code like:
public async Task Login()
{
await HttpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme,
new AuthenticationProperties() { RedirectUri = "/" });
}

ASP.NET Core Identity uses wrong NameClaimType with OpenIdConnect

I'm using the OpenIdConnect middleware to do authentication with AzureAD. And Identity to handle all these.
In the scaffolded ExternalLogin page I receive a correct SignUpInfo where the principle name is as expected (Fullname). With the default implementation, Identity discard this information and uses the email address as the principle name. The respective principal name (Fullname) gets never persisted.
I've seen that these two uses different NameClaimTypes. But configuring it at AddIdentity(...) or AddOpenIdConnect(...) does not change anything.
How should I handle this?
Update
Startup.cs
var authenticationMethods = services.AddAuthentication()
.AddOpenIdConnect(authenticationScheme, authenicationDisplayName, options =>
{
options.ClientId = azureAdOptions.ClientId;
options.ClientSecret = azureAdOptions.ClientSecret;
options.Authority = $"{azureAdOptions.Instance}common/v2.0";
options.CallbackPath = azureAdOptions.CallbackPath;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = $"{azureAdOptions.Instance}{azureAdOptions.TenantId}/v2.0",
NameClaimType = "name"
};
options.Events.OnRemoteFailure = context =>
{
logger.LogError(context.Failure, "Fehler beim Anmelden via XYZ");
context.Response.Redirect("/");
context.HandleResponse();
return Task.CompletedTask;
};
azureAdOptions.Scopes.ForEach(options.Scope.Add);
});
// Fügt die ASP.NET CORE Identity Middleware hinzu
services.AddIdentity<TUser, IdentityRole>()
.AddRoleManager<RoleManager<TRole>>()
.AddDefaultUI()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<TContext>();
ExternalLogin.cshtml.cs (default scaffolded code)
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true);
if (result.Succeeded)
{
await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
_logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
return LocalRedirect(returnUrl);
}
Here, the info.Principal.Identity.Name represents the readable fullname which has ben derived from the NameClaimTypes 'name' from OpenIdConnect. But after the login, the Identity uses 'XML-Namespace' for NameClaimTypes and in each Controller the User.Identity.Name resolves to the username / email address.

WWW-Authenticate header in Identity Server 4 Unauthorized response

I'm protecting a Web API with Identity Server 4.
If an external app tries to access it using client credentials but does not pass in the access token, I get, as expected, the Unauthorized response.
The problem here is that the response does not include the WWW-Authenticate header as I was expecting, as stated in the OAuth spec.
Am I missing some config in Identity Server? Or is it something wrong with the Identity Server implementation?
The relevant code parts follow:
Client registration on Identity Server:
new Client()
{
ClientId = "datalookup.clientcredentials",
ClientName = "Data Lookup Client with Client Credentials",
AlwaysIncludeUserClaimsInIdToken = true,
AlwaysSendClientClaims = true,
AllowOfflineAccess = false,
ClientSecrets =
{
new Secret("XXX".Sha256())
},
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes =
{
Scopes.DataLookup.Monitoring,
Scopes.DataLookup.VatNumber
},
ClientClaimsPrefix = "client-",
Claims =
{
new Claim("subs", "1000")
}
}
ApiResource registration on Identity Server:
new ApiResource()
{
Name = "datalookup",
DisplayName = "Data Lookup Web API",
ApiSecrets =
{
new Secret("XXX".Sha256())
},
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Email,
JwtClaimTypes.Profile,
"user-subs"
},
Scopes =
{
new Scope()
{
Name = Scopes.DataLookup.Monitoring,
DisplayName = "Access to the monitoring endpoints",
},
new Scope()
{
Name = Scopes.DataLookup.VatNumber,
DisplayName = "Access to the VAT Number lookup endpoints",
Required = true
}
}
}
Authentication configuration in the Web API:
public void ConfigureServices(IServiceCollection services)
{
(...)
services.AddMvc();
services
.AddAuthorization(
(options) =>
{
options.AddPolicy(
Policies.Monitoring,
(policy) =>
{
policy.RequireScope(Policies.Scopes.Monitoring);
});
options.AddPolicy(
Policies.VatNumber,
(policy) =>
{
policy.RequireScope(Policies.Scopes.VatNumber);
policy.RequireClientSubscription();
});
});
services.AddAuthorizationHandlers();
services
.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(
(options) =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "datalookup";
});
(...)
}
Client accessing the Web API:
using (HttpClient client = new HttpClient())
{
// client.SetBearerToken(accessToken);
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Constants.WebApiEndpoint))
{
using (HttpResponseMessage response = await client.SendAsync(request).ConfigureAwait(false))
{
if (!response.IsSuccessStatusCode)
{
ConsoleHelper.WriteErrorLine(response);
return;
}
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
ConsoleHelper.WriteInformationLine(content);
}
}
}
Notice that client.SetBearerToken(accessToken) is commented, so that is why I was expecting the response to include the WWW-Authenticate header.
The whole idea behind this is to implement a feature on a client library to deal with the Http Bearer challenge (as, for example, the Azure KeyVault client library does).

Notifications in ASP.NET Core client for IdentityServer v4

In IdentityServer 3 I used the SecurityTokenValidated event on the Notifications to build up my own identity with names and claims. For example I store the access_token to later access n API with a resource owner workflow like this:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
// ...
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
"name",
ClaimTypes.Role);
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
}
}
}
In IdentityServer 4 for ASP.NET Core is not Notifications property.
I can see that there a lot of claims automatically generated but I don't get the access_token nor the username of the identity is set automatically
My current configuration of the client in ASP.NET Core looks like this
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = identityServerUri,
RequireHttpsMetadata = false,
ClientId = clientId,
ResponseType = "id_token token",
Scope =
{
"openid profile email warehouseapi"
},
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
});
What is the intended way in IdentityServer 4 to make this ?
You can use TickedReceived event to transform claims:
var oidcOptions = new OpenIdConnectOptions
{
...
Events = new OpenIdConnectEvents()
{
// get access token
OnTicketReceived = ctx =>
{
// transform claims
var access_token = ctx.Ticket.Properties.GetTokenValue("access_token");
return Task.FromResult(0);
}
}
};
Also you don't need save tokens as claims, because when you set SaveTokens to true, tokens are automatically saved in the authentication properties. To get a token, you can use HttpContext.Authentication.GetTokenAsync("<token name>").
This is actually not related to IdentityServer4. It is rather more of a difference between the Authentication middlewares in the OWIN and AspNetCore variants.
These notifications are now more correctly named Events
you can do something similar using:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = "https://demo.identityserver.io",
PostLogoutRedirectUri = "http://localhost:3308/",
ClientId = "hybrid",
ClientSecret = "secret",
ResponseType = "code id_token",
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
Events = new OpenIdConnectEvents
{
OnTokenValidated = async n =>
{
}
}
});
You can find all the nice events here.