after searching for two days to trying and identify where I am going wrong, I have accepted that I need some help to point me in the right direction.
I'm at the really early stages of working with Identity server, still simply using inMemory clients and scopes, just to get my head around what is happening and how it all links together.
I am trying to return a list of custom claims to my angular application from Identity server, but I am failing. I've tried extending IProfileService, which successfully adds the custom claim but it removes the other claims, that I defined in my TestUser
With MyProfileService registered
Without MyProfileService registered
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(Resources.GetApiResources())
.AddInMemoryIdentityResources(Resources.GetIdentityResources())
.AddInMemoryClients(Clients.Get())
.AddTestUsers(Users.Get())
.AddDeveloperSigningCredential();
//services.AddTransient<IProfileService, MyProfileService>();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
#if DEBUG
app.UseDeveloperExceptionPage();
#endif
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
MyProfileService.cs
public class MyProfileService : IProfileService
{
public MyProfileService()
{
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// Issue custom claim
context.IssuedClaims.Add(new System.Security.Claims.Claim("TenantId", "123456"));
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.CompletedTask;
}
}
Resources.cs
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource> {
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource {
Name = "role",
UserClaims = new List<string> {"role"}
},
new IdentityResource
{
Name = "tenant.info",
DisplayName = "Tenant Information",
UserClaims = new List<string>
{
"tenantid",
"subscriptionid"
}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource> {
new ApiResource("api1", "api1")
};
}
Users.cs
public static List<TestUser> Get()
{
return new List<TestUser> {
new TestUser {
SubjectId = "5BE86359-073C-434B-AD2D-A3932222DABE",
Username = "scott",
Password = "password",
Claims = new List<Claim>
{
new Claim("tenantid", "123456"),
new Claim(JwtClaimTypes.Name, "Scott xxxxx"),
new Claim(JwtClaimTypes.GivenName, "Scott"),
new Claim(JwtClaimTypes.FamilyName, "xxxxx"),
new Claim(JwtClaimTypes.Email, "Scottxxxxx#email.com"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
new Claim(JwtClaimTypes.Address, #"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json)
}
}
};
}
Clients.cs
public static IEnumerable<Client> Get()
{
return new List<Client> {
new Client {
ClientId = "angular_spa",
ClientName = "Angular 4 Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
AllowedScopes = new List<string> {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
RedirectUris = new List<string> { "http://localhost:4200/admin/loggedin" },
PostLogoutRedirectUris = new List<string> { "http://localhost:4200/admin/loggedout" },
AllowedCorsOrigins = new List<string> { "http://localhost:4200" },
AllowAccessTokensViaBrowser = true
}
};
}
EDIT:
Additional failed solutions
Add default behaviour to MyProfileService (as suggested by the answer from Ruard van Elburg)
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.AddRequestedClaims(context.Subject.Claims);
context.IssuedClaims.Add(new System.Security.Claims.Claim("tenantId", "123456"));
}
Result in client: shows the tenantId but no other claims that I set on my TestUser
profile:
amr: ["pwd"]
auth_time: 1553024858
idp: "local"
sid: "34f36d1c0056ad3d65d1671e339e73aa"
sub: "5BE86359-073C-434B-AD2D-A3932222DABE"
tenantId: "123456"
__proto__: Object
Add subject.claims to issedClaims
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.IssuedClaims.Add(new System.Security.Claims.Claim("tenantId", "123456"));
context.IssuedClaims.AddRange(context.Subject.Claims);
}
Result in client: shows the tenantId and name (which is referring to the username) but no claims that I set on my TestUser
profile:
amr: ["pwd"]
auth_time: 1553025311
idp: "local"
name: "scott"
sid: "831a89053b54f3df7c9ca1bca92e1e10"
sub: "5BE86359-073C-434B-AD2D-A3932222DABE"
tenantId: "123456"
Define custom identity resources (resources docs)
I removed MyProfileService and added
public static IEnumerable<IdentityResource> GetIdentityResources()
{
var customProfile = new IdentityResource(
name: "custom.profile",
displayName: "Custom profile",
claimTypes: new[] {
"name",
"given_name",
"family_name",
"email",
"email_verified",
"website",
"address",
"status",
"tenantid" });
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
customProfile
};
}
Result in client I do not see all the claim types
profile:
amr: ["pwd"]
auth_time: 1553026892
family_name: "FamilyName"
given_name: "Scott givenName"
idp: "local"
name: "Scott name"
sid: "47ae7f9b5240742e2b2b94a739bed5fa"
sub: "5BE86359-073C-434B-AD2D-A3932222DABE"
website: "http://scott.com"
The problem is that you've removed default behaviour. So you'll need to restore that by adding the following line to your profile service (which is present in the DefaultProfileService):
context.AddRequestedClaims(context.Subject.Claims);
But it is not necessary to implement your own IProfileService. In this case you can suffice by configuring the scope for the client:
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"tenant.info",
"api1"
},
And requesting the scope in the client:
options.Scope.Add("tenant.info");
This should be enough to include the tenantId claim.
Related
When i'm trying to sign-in with google sign-in from blazor webassembly, the requested claims and the claims returned from identity server is not matching (see output)
BackEnd/Config.cs
namespace BackEnd
{
public static class ServerConfiguration
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource("roles", "User roles", new List<string> { "role" })
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("protectedScope", "Protected Scope")
};
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client()
{
ClientId = <confidential>,
ClientName = "client 1",
RequireClientSecret = false,
RequirePkce = true,
AllowedCorsOrigins = { "https://localhost:5001" },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:5001/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:5001/" },
AllowOfflineAccess = true,
AllowedScopes = new List<string>{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"protectedScope"
}
},
new Client()
{
ClientId = "blazor",
ClientName = "oidcUser",
RequireClientSecret = false,
RequirePkce = true,
RequireConsent = true,
AllowedCorsOrigins = { "https://localhost:5001" },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:5001/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:5001/" },
AllowOfflineAccess = true,
AlwaysIncludeUserClaimsInIdToken = true,
AllowedScopes = new List<string>{
"openid",
"email",
"profile",
"protectedScope"
}
}
};
public static List<TestUser> TestUsers {
get
{
TestUser user1 = new TestUser()
{
SubjectId = "2f47f8f0-bea1-4f0e-ade1-88533a0eaf57",
Username = "John",
Claims = new List<Claim>()
{
new Claim("role", "SignedInUser"),
new Claim("email", "johnsmith#gmail.com"),
new Claim("picture", "https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.business2community.com%2Fsocial-media%2Fimportance-profile-picture-career-01899604&psig=AOvVaw2LC5T-WZMYnHD9I7PeK7lT&ust=1615219065948000&source=images&cd=vfe&ved=2ahUKEwip1caGxp7vAhV1NbcAHd_2BFwQjRx6BAgAEAc")
}
};
List<TestUser> testUsers = new List<TestUser>();
testUsers.Add(user1);
return testUsers;
}
}
}
}
BackEnd/Startup.cs
namespace BackEnd
{
public class Startup
{
public IWebHostEnvironment Environment { get; }
public IConfiguration Configuration { get; }
private string _clientId = null;
private string _clientSecret = null;
public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
Environment = environment;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
var cert = new X509Certificate2(Path.Combine(".", "IdsvCertificate.pfx"), "YouShallNotPass123");
_clientId = Configuration["OAuth:ClientId"];
_clientSecret = Configuration["OAuth:ClientSecret"];
services.AddControllersWithViews();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddTransient<IProfileService, ProfileService>();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
options.UserInteraction = new UserInteractionOptions() { LoginUrl = "/Account/Login", LogoutUrl = "/Account/Logout" };
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddProfileService<ProfileService>()
.AddAspNetIdentity<ApplicationUser>();
builder.AddSigningCredential(cert);
// builder.AddDeveloperSigningCredential();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = _clientId;
options.ClientSecret = _clientSecret;
options.ClaimActions.MapJsonKey("picture", "picture", "url");
options.SaveTokens = true;
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.Authority = "https://accounts.google.com";
options.RequireHttpsMetadata = true;
options.ResponseType = "code";
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("openid");
options.ClientId = _clientId;
options.ClientSecret = _clientSecret;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapJsonKey("picture", "picture", "url");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
ValidateIssuer = true
};
});
services.AddAuthorization();
services.AddGrpc();
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<UserService>();
endpoints.MapDefaultControllerRoute().RequireAuthorization();
});
}
}
}
BackEnd/ProfileService.cs
public class ProfileService : IProfileService
{
public ProfileService()
{
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
context.IssuedClaims.AddRange(roleClaims);
await Task.CompletedTask;
}
public async Task IsActiveAsync(IsActiveContext context)
{
await Task.CompletedTask;
}
}
FrontEnd/Program.cs
namespace FrontEnd
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient()
{ BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped(services =>
{
var httpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
var channel = GrpcChannel.ForAddress("https://localhost:5000", new GrpcChannelOptions
{
HttpHandler = httpHandler
});
return new Greeter.GreeterClient(channel);
});
builder.Services.AddScoped(services =>
{
var baseAddressMessageHandler = services.GetRequiredService<BaseAddressAuthorizationMessageHandler>();
baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
var httpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
var channel = GrpcChannel.ForAddress("https://localhost:5000", new GrpcChannelOptions
{
HttpHandler = httpHandler
});
return new User.UserClient(channel);
});
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("Authentication:Google", options.ProviderOptions);
options.UserOptions.RoleClaim = "SignedInUser";
}).AddAccountClaimsPrincipalFactory<CustomUserFactory>();
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
await builder.Build().RunAsync();
}
}
FrontEnd/wwwroot/appsettings.json
{
"Authentication":{
"Google": {
"Authority": "https://localhost:5000",
"ClientId": <confidential>,
"ClientSecret": "2fxc9srOe8QsRBnhzLIa1pF0",
"DefaultScopes": [
"email",
"profile",
"openid"
],
"PostLogoutRedirectUri": "https://localhost:5001/",
"RedirectUri": "https://localhost:5001/authentication/login-callback",
"ResponseType": "code"
},
}
This is the output from IdentityServer when i'm trying to sign-in
[18:34:28 Debug] IdentityServer4.Validation.TokenValidator
Calling into custom token validator: IdentityServer4.Validation.DefaultCustomTokenValidator
[18:34:28 Debug] IdentityServer4.Validation.TokenValidator
Token validation success
{"ClientId": null, "ClientName": null, "ValidateLifetime": true, "AccessTokenType": "Jwt", "ExpectedScope": "openid", "TokenHandle": null, "JwtId": "8E6167D64F8FEA2FF6D12D17A1CEEBFE", "Claims": {"nbf": 1615548868, "exp": 1615552468, "iss": "https://localhost:5000", "aud": "https://localhost:5000/resources", "client_id": "499675830263-ldcg4fm7kcbjlt48tpaffqdbfnskmi8v.apps.googleusercontent.com", "sub": "81c306df-c1f0-4714-964f-2459b670429e", "auth_time": 1615548849, "idp": "oidc", "jti": "8E6167D64F8FEA2FF6D12D17A1CEEBFE", "sid": "0BF0BA4CA6BD3DE8D158A426A70A91E0", "iat": 1615548868, "scope": ["openid", "profile", "email", "role"], "amr": "external"}, "$type": "TokenValidationLog"}
[18:34:28 Debug] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Creating userinfo response
[18:34:28 Debug] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Scopes in access token: openid profile email role
[18:34:28 Debug] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Requested claim types: sub name family_name given_name middle_name nickname preferred_username profile picture website gender birthdate zoneinfo locale updated_at email email_verified role
[18:34:28 Information] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Profile service returned the following claim types: sub name preferred_username
[18:34:28 Debug] IdentityServer4.Endpoints.UserInfoEndpoint
End userinfo request
[18:34:29 Debug] IdentityServer4.Hosting.EndpointRouter
Request path /connect/checksession matched to endpoint type Checksession
[18:34:29 Debug] IdentityServer4.Hosting.EndpointRouter
Endpoint enabled: Checksession, successfully created handler: IdentityServer4.Endpoints.CheckSessionEndpoint
[18:34:29 Information] IdentityServer4.Hosting.IdentityServerMiddleware
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.CheckSessionEndpoint for /connect/checksession
[18:34:29 Debug] IdentityServer4.Endpoints.CheckSessionEndpoint
Rendering check session result
From the output, the requested claims and the claims that's returned by the profile service is not matching
From the image, there should be a name after the "hello"
The image is similar except now it shows the name. IdentityServer4 is working fine, it shows everything as i expected
By default, only these claims are mapped from the ID-Token:
options.ClaimActions.MapUniqueJsonKey("sub", "sub");
options.ClaimActions.MapUniqueJsonKey("name", "name");
options.ClaimActions.MapUniqueJsonKey("given_name", "given_name");
options.ClaimActions.MapUniqueJsonKey("family_name", "family_name");
options.ClaimActions.MapUniqueJsonKey("profile", "profile");
options.ClaimActions.MapUniqueJsonKey("email", "email");
To get additional claims mapped, we have to write like:
options.ClaimActions.MapUniqueJsonKey("website", "website");
options.ClaimActions.MapUniqueJsonKey("gender", "gender");
options.ClaimActions.MapUniqueJsonKey("birthdate", "birthdate");
In AddOpenIDConnect. This is called Claims transformation if you want to google for it.
the issue is that i have a project with .net core 3.1 and reactjs called tv-participants-lookup and after implementing and configuring serveridentity4 and when trying to register or login it returns an Invalid Scope error, hence the problem is as shown bellow in the .well-known/openid-configuration:
{
"issuer": "https://localhost:5001",
"jwks_uri": "https://localhost:5001/.well-known/openid-configuration/jwks",
"authorization_endpoint": "https://localhost:5001/connect/authorize",
"token_endpoint": "https://localhost:5001/connect/token",
"userinfo_endpoint": "https://localhost:5001/connect/userinfo",
"end_session_endpoint": "https://localhost:5001/connect/endsession",
"check_session_iframe": "https://localhost:5001/connect/checksession",
"revocation_endpoint": "https://localhost:5001/connect/revocation",
"introspection_endpoint": "https://localhost:5001/connect/introspect",
"device_authorization_endpoint": "https://localhost:5001/connect/deviceauthorization",
"frontchannel_logout_supported": true,
"frontchannel_logout_session_supported": true,
"backchannel_logout_supported": true,
"backchannel_logout_session_supported": true,
"scopes_supported": [
"openid",
"profile",
"API.ClientId",
"TV Participants lookupAPI", **<=======** you may notice the space between the words causing it to appear like three scopes when validating the scopes
"offline_access"
],
and this is my appsettings.json:
"IdentityServer": {
"Clients": {
"client1": {
"Profile": "IdentityServerSPA",
"AlowedScopes":"openid profile"
}
},
"Resources": {
"API.ClientId": {
"Profile": "API"
}
}
},
startup.cs as follows:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString(("DefaultConnection"))));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthorization();
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.RoleClaimType = JwtClaimTypes.Role;
});
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddTransient<IProfileService, ProfileService>();
services.AddControllersWithViews();
services.AddRazorPages();
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
So the question is from where is the openid-connect is getting that scope -"TV Participants lookupAPI"-from ?? and is there a way to remove or alter the scopes_supported ?
Figured it out as I had to change the project name and remove the spaces. Once the .csproj is named TV_Participants_lookupAPI the problem has been resolved.
I have the following flow Asp.Net Core App (App) that calls Asp.Net Core Web API (API1) that inturn calls another Asp.Net Web API (API2)
I'm using IdentityServer4 with Windows Authentication to Authenticate my users
here is my code:
ApiResources definition:
new ApiResource("api1", "api1", new List<string> { JwtClaimTypes.Name, JwtClaimTypes.Email}),
new ApiResource("api2", "api2", new List<string> { JwtClaimTypes.Name, JwtClaimTypes.Email})
Clients definition
new Client
{
ClientId = "app1",
ClientName = "app1",
ClientSecrets = { new Secret("app1".Sha256())},
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
IdentityServerConstants.StandardScopes.Email,
"api1"
},
RedirectUris = { "https://localhost:44375/signin-oidc" },
FrontChannelLogoutUri = "https://localhost:44375/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:44375/signout-callback-oidc" },
AllowOfflineAccess = true,
RequireConsent = false,
AccessTokenLifetime = 5
},
new Client
{
ClientId = "api1",
ClientSecrets = { new Secret("api1".Sha256())},
AllowedGrantTypes = {"delegation" },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"api2"}
}
Delegation code in API1
public async Task<string> DelegateAsync(string userToken)
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:44382/");
if (disco.IsError) throw new Exception(disco.Error);
var tokenResponse = await client.RequestTokenAsync(new TokenRequest()
{
Address = disco.TokenEndpoint,
GrantType = "delegation",
ClientId = "api1",
ClientSecret = "api1",
Parameters =
{
{"scope" , "api2 email profile openid" },
{"token", userToken }
}
});
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
_logger.LogInformation($"new: {tokenResponse.AccessToken}");
return tokenResponse.AccessToken;
}
IdentityServer4 DelegationGrantValidator:
public class DelegationGrantValidator : IExtensionGrantValidator
{
private readonly ITokenValidator _validator;
public DelegationGrantValidator(ITokenValidator validator)
{
_validator = validator;
}
public string GrantType => "delegation";
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var userToken = context.Request.Raw.Get("token");
if (string.IsNullOrEmpty(userToken))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _validator.ValidateAccessTokenAsync(userToken);
if (result.IsError)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
// get user's identity
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
context.Result = new GrantValidationResult(sub, GrantType);
return;
}
}
in API1 User.Identity.Name = "Domain\UserName"
but in API2 User.Identity.Name = null
is there anything missing that I should do solve this issue?
P.S.: if I call the IdentityServer UserInfo endpoint from API2 I'll get the expected UserName
After a lot of search I have finally found the answer.
I've followed what Rory Braybrook described in this Article and everything is working now
There are four clients within the application:
angular.application - resource owner
identity_ms.client - webapi app (.net core 2.1)
IdentityServer4 with AspNetIdentity
AccountController with shared actions to register users, reset password etc.
UserController with secured actions.
The Data action of the UserController has an [Authorize(Policy = "user.data")] attribute
ms_1.client - webapi app (.net core 2.1)
request.client - added specially to send requests from ms_1.client to identity_ms.client's UserController to get some user data.
I'm requesting clients using Postman:
http://localhost:identity_ms_port/connect/token to get access_token
http://localhost:ms_1_port/api/secured/action to get some secured data from ms_1
http://localhost:identity_ms_port/api/user/data to get some secured user data from identity_ms
Everything is working fine.
Also, ms_1 service has a secured action requesting http://localhost:identity_ms_port/api/user/data using System.Net.Http.HttpClient.
// identity_ms configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(/*cors options*/);
services
.AddMvc()
.AddApplicationPart(/*Assembly*/)
.AddJsonOptions(/*SerializerSettings*/)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
var clients = new List<Client>
{
new Client
{
ClientId = "angular.application",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "user.data.scope", "ms_1.scope", "identity_ms.scope" },
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword
},
new Client
{
ClientId = "ms_1.client",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "user.data.scope", "ms_1.scope" },
AllowedGrantTypes = GrantTypes.ClientCredentials
},
new Client
{
ClientId = "identity_ms.client",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes =
{
"user.data.scope",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
AllowedGrantTypes = GrantTypes.Implicit
},
new Client
{
ClientId = "request.client",
AllowedScopes = { "user.data.scope" },
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
}
}
};
var apiResources = new List<ApiResource>
{
new ApiResource("ms_1.scope", "MS1 microservice scope"),
new ApiResource("identity_ms.scope", "Identity microservice scope"),
new ApiResource("user.data.scope", "Requests between microservices scope")
};
var identityResources = new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
services
.AddAuthorization(options => options.AddPolicy("user.data", policy => policy.RequireScope("user.data.scope")))
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(identityResources)
.AddInMemoryApiResources(apiResources)
.AddInMemoryClients(clients);
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = "identity_ms.scope";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:identity_ms_port";
});
services.AddSwaggerGen(/*swagger options*/);
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<CustomMiddleware>();
app.UseIdentityServer();
app.UseAuthentication();
app.UseCors("Policy");
app.UseHttpsRedirection();
app.UseMvc(/*routes*/);
app.UseSwagger();
}
// ms_1.client configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(/*cors options*/);
services
.AddMvc()
.AddJsonOptions(/*SerializerSettings*/)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Audience = "ms_1.scope";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:identity_ms_port";
});
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<CustomMiddleware>();
app.UseAuthentication();
app.UseCors("Policy");
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseMvc(/*routes*/);
app.UseSwagger();
}
// ms_1.client action using HttpClient
[HttpPost]
public async Task<IActionResult> Post(ViewModel model)
{
//...
using (var client = new TokenClient("http://localhost:identity_ms_port/connect/token", "ms_1.client", "secret"))
{
var response = await client.RequestClientCredentialsAsync("user.data.scope");
if (response.IsError)
{
throw new Exception($"{response.Error}{(string.IsNullOrEmpty(response.ErrorDescription) ? string.Empty : $": {response.ErrorDescription}")}", response.Exception);
}
if (string.IsNullOrWhiteSpace(response.AccessToken))
{
throw new Exception("Access token is empty");
}
var udClient = new HttpClient();
udClient.SetBearerToken(response.AccessToken);
udClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var result = await udClient.GetAsync("http://localhost:identity_ms_port/api/user/data");
}
//...
}
I've tried the following:
To retrieve access_token from the request to ms_1 Authorization header and use it to access user/data.
To get new access_token to access user/data with it.
See public async Task<IActionResult> Post(ViewModel model) code within the code block.
In both cases, I've got the correct token which I can use to request both secured/action and user/data actions from Postman, but HttpClient is getting Unauthorized response (401).
Response headers screenshot
What am I doing wrong?
In your client code with HttpClient you are not requesting any scopes for the API therefore the token that is issued by Identity Server 4 will not contain the API as one of the audiences and then subsequently you will get 401 from API.
Change your token request to ask for the API scope as well.
var response = await client.RequestClientCredentialsAsync("user.data.scope ms_1.scope");
I am new to Identity Server. I haven't configured it before. But I need it for a Project I am working on.
The API will be serving an Angular JS Client, iOS App and an Android App. We need to implement authentication and authorisation and custmer grant
Note: I am trying to configure Identity Server and my API in the same Web API project.
I have followed the documentation and configured Identity Server as the following:
In startup.cs, in ConfigureServices()
private readonly IConfiguration config;
private const string DEFAULT_CORS_POLICY = "localhost";
public Startup (IConfiguration config) => this.config = config;
public void ConfigureServices (IServiceCollection services) {
services.AddIdentityServer ()
.AddDeveloperSigningCredential ()
//.AddInMemoryApiResources(config.GetSection("ApiResources"))
.AddInMemoryApiResources (Config.GetApis ())
//.AddInMemoryClients(config.GetSection("Clients"))
.AddInMemoryClients (Config.GetClients ())
.AddInMemoryIdentityResources (Config.GetIdentityResources ())
//.AddInMemoryIdentityResources(config.GetSection("IdentityResources"))
.AddExtensionGrantValidator<WechatGrantValidator> ();
services.AddTransient<IUserCodeValidator, UserCodeValidator> ();
services.AddCors (options => {
options.AddPolicy (DEFAULT_CORS_POLICY, builder => {
builder.WithOrigins ("http://localhost:5202");
builder.AllowAnyHeader ();
builder.AllowAnyMethod ();
});
});
}
I implemented the interface IExtensionGrantValidator and register the extension grant
public class WechatGrantValidator : IExtensionGrantValidator {
private IUserCodeValidator validator;
public WechatGrantValidator (IUserCodeValidator validator) {
this.validator = validator;
}
public string GrantType => "wechat_grant";
public async Task ValidateAsync (ExtensionGrantValidationContext context) {
string userCode = context.Request.Raw.Get ("userCode");
var result = await validator.ValidateAsync (userCode);
if (result.IsError) {
context.Result = new GrantValidationResult (TokenRequestErrors.InvalidGrant);
return;
}
context.Result = new GrantValidationResult (result.UserId, GrantType);
return;
}
}
I have followed the documentation and configured client infos as the following
public static IEnumerable<Client> GetClients () {
return new Client[] {
new Client {
ClientId = "javascritpClient",
ClientName = "JavaScript Client",
AllowedGrantTypes = { "wechat_grant" },
AllowAccessTokensViaBrowser = true,
AllowedCorsOrigins = { "http://localhost:5202" },
AllowedScopes = { "api1" },
ClientSecrets = { new Secret ("secret".Sha256 ()) }
}
};
}
Now because I want to use it Angular JS, iOS and Android I want to just get the Access Token from the IdentityServer, and then use the Access Token for Authentication and Authorisation.
for this I am trying to access the /connect/token from a JS client
But I am getting an invalid_client error.
#Injectable()
export class OauthService {
private http: Http;
public constructor(http: Http) {
this.http = http;
}
public async getDiscoveryInfos(issuer: string): Promise<DiscoveryInfos> {
if (!issuer.endsWith('/')) {
issuer += '/';
}
issuer += '.well-known/openid-configuration';
return this.http.get(issuer).map(response => {
return response.json();
}).toPromise();
}
public async getToken(): Promise<any> {
const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" });
const discovery = await this.getDiscoveryInfos('http://localhost:5200');
return this.http.post(discovery.token_endpoint, {
grant_type: 'wechat_grant',
userCode: 'userCodeAA',
client_id: 'javascritpClient',
client_secret: 'secret',
scope:'api1'
}, { headers: headers }).map(response => response.json()).toPromise();
}
}
http response infos
The server response "error":"invalid_client"
log infos
The error I get on the server side is 'No client identifier found':
1 - Why am I getting this error?
2 - As I need to get the Token programmatically in JS, I need to use /connect/token, am I correct on this? Am I on the correct path?
in ng2 use a method like bellow:
public Token(data: SigninModel): Observable<any> {
this.options = new RequestOptions({ headers: this.headers });
this.headers.append('Content-Type', 'application/x-www-form-urlencoded');
const url = this.urlBase + `connect/token`;
const param = new URLSearchParams();
param.set('grant_type', 'password');
param.set('client_Id', 'javascritpClient');
param.set('client_secret', 'secret');
param.set('scope', 'offline_access');
param.set('username', data.username);
param.set('password', data.password);
return this.http.post(url, `${param.toString()}`, this.options)
.map((response: Response) => {
return (response.json());
})
.catch(this.handleError);
}