I have 2 ASP.NET Core 2.1 applications (Auth and API) using Identity Server 4.
On API application Startup I have the following:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(x =>
x.ApiName = "api";
x.Authority = "https://localhost:5005";
x.RequireHttpsMetadata = false;
});
On Auth application Startup I have the following configuration:
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetTestUsers());
Where Config class is the following:
public class Config {
public static List<ApiResource> GetApiResources() {
return new List<ApiResource> {
new ApiResource("api", "API Resource")
};
}
public static List<IdentityResource> GetIdentityResources() {
return new List<IdentityResource> {
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
public static List<Client> GetClients() {
return new List<Client> {
new Client {
ClientId = "app",
ClientName = "APP Client",
ClientSecrets = { new Secret("pass".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = {
"api"
}
}
}
public static List<TestUser> GetTestUsers() {
return new List<TestUser> {
new TestUser {
SubjectId = "1",
Username = "john",
Password = "john",
Claims = new List<Claim> { new Claim("name", "John") } },
};
}
}
With Postman I am able to access .well-known/openid-configuration and create an Access Token.
However, when I call the API I get the following error:
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[3]
Exception occurred while processing message.
System.InvalidOperationException: IDX20803: Unable to obtain configuration from: '[PII is hidden by default.
Set the 'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]'. ---> System.IO.IOException: IDX20804: Unable to retrieve document from: '[PII is hidden by default.
Set the 'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]'. ---> System.Net.Http.HttpRequestException:
The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.
I ran dotnet dev-certs https --trust; and the localhost certificate appears on Mac Keychain.
Important
On a Windows computer the code I posted works fine.
I am able to access .well-known/openid-configuration and the API endpoint.
What am I missing?
Related
I am implementing Identity Server in a razor page application.
When requesting the speech ApiResource, identityserver returns "invalid_scope". My understanding is that the resource is a group of scopes. So, I was expecting the identityserver to return the scopes defined in the speech resource.
Note: Which I add speech as ApiScope it works fine but then it does not add the speech.synthesize and payment.subscription scopes.
Here's how I have defined the ApiScopes:
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("speech.synthesize", "Speech synthesis",new []{"api.create" }),
new ApiScope("payment.subscription", "Subscription service"),
new ApiScope("payment.manage", "Manage Payment"),
};
And here's how I have defined the ApiResource:
public static IEnumerable<ApiResource> ApiResources =>
new List<ApiResource>
{
new ApiResource("speech", "Speech API")
{
Scopes = { "speech.synthesize", "payment.subscription" }
}
};
And here's the client configuration:
public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
AlwaysSendClientClaims = true,
// scopes that client has access to
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"speech"
}
}
};
What is wrong here? Can anybody help me understand the problem.
What is the role of the Api Resource if not grouping the scopes.
You as a client asks for ApiScopes, not ApiResources. One more more ApiResource can point to an ApiScope.
An ApiResource represents an API instance, not a Scope. ApiResources are like clients, but for Apis.
See my answer here for more details about the difference between IdentityResource, ApiResource and ApiScope
I have been searching for how IdentityServer4 uses DB. I have read: https://docs.identityserver.io/en/release/quickstarts/8_entity_framework.html and looked at the QuickStart4 which uses a DB store. What I can't find is how I can use it in a many clients scenario where we want to add client details to DB only without having to add the client to the config.cs in Identity Server like so:
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("api1", "My API")
};
public static IEnumerable<Client> Clients =>
new List<Client>
{
// machine to machine client
new Client
{
ClientId = "client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
// scopes that client has access to
AllowedScopes = { "api1" }
},
// interactive ASP.NET Core MVC client
new Client
{
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// where to redirect to after login
RedirectUris = { "https://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
};
}
}
see this page Entity Framework Integration and this page Entity Framework Support
Basically, what you need to do is:
Add this NuGet package IdentityServer4.EntityFramework
Apply the migrations to create the necessary tables or use the pre-made SQL scripts here
Add the AddConfigurationStore to your startup class
Alternatively, you implement your own IClientStore implementation.
I'm trying to implement client certificate authentication with ASP.NET and IdentityServer4, but can't seem to make it work. Through Postman I get "Error: invalid_client", in debug console "Client secret validation failed for client: ISCCA.". I'm running the application with Kestrel on localhost.
Based on documentation and examples i've been through, this is my result so far:
Kestrel configuration:
webBuilder.ConfigureKestrel(builderOptions => {
builderOptions.ConfigureHttpsDefaults(httpOptions => {
httpOptions.AllowAnyClientCertificate();
httpOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
httpOptions.CheckCertificateRevocation = false;
});
});
Identity server configuration with in memory clients, resources and scopes:
services
.AddIdentityServer(options => {
// MTLS for client certificate authentication endpoints with default scheme set as Certificate
options.MutualTls.Enabled = true;
options.MutualTls.ClientCertificateAuthenticationScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
// Use subdomain endpoints (mtls.host)
options.MutualTls.DomainName = "mtls";
})
.AddMutualTlsSecretValidators() // So that Identity Server knows to validate thumbprint or certificate name
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(new List<ApiResource> {
new ApiResource(
name: "MyAPI", // Api resource name
displayName: "My API Set", // Display name
userClaims: new List<string> { "access" } // Claims to be included in access token
)
})
.AddInMemoryIdentityResources(GetIdentityResources()) // Contains only IdentityResources.OpenId()
.AddInMemoryClients(new List<Client>() {
new Client {
Enabled = true,
ClientId = "ISCCA",
ClientSecrets = {
// Testing env client certificate thumbprint secret
new Secret() {
Value = "<thumbprint>",
Type = SecretTypes.X509CertificateThumbprint
}
},
AccessTokenLifetime = 60 * 60 * 24,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "MyAPI" }
}
})
.AddInMemoryApiScopes(new List<ApiScope> {
new ApiScope {
Name = "MyAPI",
DisplayName = "Some API",
UserClaims = { "access" }
}
});
Authentication and authorization:
services
.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options => {
options.AllowedCertificateTypes = CertificateTypes.All;
options.RevocationMode = X509RevocationMode.NoCheck;
})
.AddIdentityServerJwt();
services.AddAuthorization(options => {
options.AddPolicy("ApiScope", policy => {
policy.RequireAuthenticatedUser();
policy.RequireClaim("access");
});
});
If i use a secret without defined type, the token is returned as expected, but when i want it to use the thumbprint, i get errors above.
I have set up the certificate in Postman and it is included in request, but i'm not sure if it comes to the server (everything is run localy on the same PC). As for token request and server response, below are screenshots of what is in auth header and response and Kestrel log:
I don't know what i did wrong. Also i have included
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
in Configure method.
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).
I have set-up identityserver3 and MVC4 client using this tutorial. When I configured client to use 'Implicit' flow things are working as expected and I am getting back 'profile' scope. i.e. I can find claims first_name and given_name. Below my configuration code.
Client and User configuration
public static class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Username = "Bob",Password = "password",Subject = "1",
Claims = new []
{
new Claim(Constants.ClaimTypes.GivenName,"firstName"),
new Claim(Constants.ClaimTypes.FamilyName,"lastName")
}
}
};
}
}
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
ClientId = "MVC",
ClientName = "MVC Client Name",
RedirectUris = new List<string>
{
"https://localhost:44302/"
},
Flow = Flows.Implicit,
AllowAccessToAllScopes = true
}
};
}
}
Identity Server Configuration
public void Configuration(IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.Map("/identity", appBuilder => {
appBuilder.UseIdentityServer(new IdentityServer3.Core.Configuration.IdentityServerOptions
{
SiteName = "Site Name",
SigningCertificate = LoadCertificate(),
RequireSsl = false,
Factory = new IdentityServer3.Core.Configuration.IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryUsers(Users.Get())
.UseInMemoryScopes(StandardScopes.All)
});
});
app.UseCookieAuthentication(new Microsoft.Owin.Security.Cookies.CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44302/identity",
ClientId = "MVC",
RedirectUri = "https://localhost:44302/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
Scope = "openid profile"
});
}
In my MVC application I have secured Action on Home controller named 'Contact'
[Authorize]
public ActionResult Contact()
{
ClaimsPrincipal principal = User as ClaimsPrincipal;
return View(principal.Claims);
}
And finally here is simple view
#model IEnumerable<System.Security.Claims.Claim>
#foreach (var item in Model)
{
<div>
<span>#item.Type</span>
<span>#item.Value</span>
</div>
}
</div>
Now when I run this app, after clicking on secure 'Contact' link I am being redirected to STS server and after providing credentials I can see below output.
Note that claims given_name and family_name exists in the claims returned by STS.
Problem:
The moment I change Client to support Hybrid flow. I am not getting back claims given_name and family_name
I made below changes to my code.
Client configuration
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
ClientId = "MVC",
ClientName = "MVC Client Name",
RedirectUris = new List<string>
{
"https://localhost:44302/"
},
Flow = Flows.Hybrid,//Changed this to Hybrid
AllowAccessToAllScopes = true
}
};
}
Server Configuration
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44302/identity",
ClientId = "MVC",
RedirectUri = "https://localhost:44302/",
ResponseType = "code id_token token", //Changed response type
SignInAsAuthenticationType = "Cookies",
Scope = "openid profile"
});
After running applicaton I can see below claims returned by STS
Note that claims given_name and family_name are missing this time.
Have I missed anything?
When you only ask for an id_token all the claims for the user are in the id_token. When you change your request to get a token (either by asking for code or token) then only the user claims configured as "AlwaysInclude" are included in the id_token. The rest must be retrieved from the user info endpoint using the access_token you received. You can use the helper APIs in the IdentityModel library to easily access the user info endpoint. Our samples show how you can do this: https://github.com/IdentityServer/IdentityServer3.Samples/blob/master/source/Clients/MVC%20OWIN%20Client%20(Hybrid)/Startup.cs#L66