IdentityServer4 with Resource API in .NET 4.5 (OWIN) - c#

I've read through numerous samples, as well as the IdentityServer 4 documentation, but I still seem to be missing something.
Basically, I have IdentityServer4 working to the point that it is proving me an AccessToken and a RefreshToken. I then try to use that AccessToken and sent an HTTP request to my WebAPI2 (.NET 4.5, OWIN), which uses IdentityServer3.AccessTokenValidation which should be compatible based on samples/tests at https://github.com/IdentityServer/CrossVersionIntegrationTests/
The WebAPI2 is giving me at HTTP 400 when I try to access a resource which required Authorization, and I am truly clueless as to why it happens.
Here is the code:
QuickstartIdentityServer Startup.cs
public void ConfigureServices(IServiceCollection services)
{
var connectionString = #"server=(localdb)\mssqllocaldb;database=IdentityServer4.Quickstart.EntityFramework;trusted_connection=yes";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
// configure identity server with in-memory stores, keys, clients and scopes
var identityServerConfig = services.AddIdentityServer()
.AddConfigurationStore(builder =>
builder.UseSqlServer(connectionString, options =>
options.MigrationsAssembly(migrationsAssembly)))
.AddOperationalStore(builder =>
builder.UseSqlServer(connectionString, options =>
options.MigrationsAssembly(migrationsAssembly)))
.AddSigningCredential(new X509Certificate2(Path.Combine(_environment.ContentRootPath, "certs", "IdentityServer4Auth.pfx"), "test"));
identityServerConfig.Services.AddTransient<IResourceOwnerPasswordValidator, ActiveDirectoryPasswordValidator>();
identityServerConfig.Services.AddTransient<IProfileService, CustomProfileService>();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
InitializeDatabase(app);
app.UseDeveloperExceptionPage();
app.UseIdentityServer();
app.UseMvcWithDefaultRoute();
}
QuickstartIdentityServer Config.cs (that I used to seed my database)
public class Config
{
// scopes define the API resources in your system
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
{
Scopes = new [] { new Scope("api1"), new Scope("offline_access") },
UserClaims = { ClaimTypes.Role, "user" }
}
};
}
// client want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" }
},
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
UpdateAccessTokenClaimsOnRefresh = true,
AllowedScopes = { "api1", "offline_access" },
AbsoluteRefreshTokenLifetime = 86400,
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.ReUse
}
};
}
}
WebAPI2 Startup.cs
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:44340/",
RequiredScopes = new[] { "api1" },
DelayLoadMetadata = true
});
WebApiConfig.Register(config);
app.UseWebApi(config);
}
WebAPI2 TestController
public class TestController : ApiController
{
// GET: api/Test
[Authorize]
public async Task<IHttpActionResult> Get()
{
return Json(new { Value1 = "value1", Value2 = "value2" });
}
}
ConsoleApplication to test this:
private static async Task MainAsync()
{
// discover endpoints from metadata
//DiscoveryClient client = new DiscoveryClient("https://dev-ea-authapi");
DiscoveryClient client = new DiscoveryClient("http://localhost:44340/");
client.Policy.RequireHttps = false;
var disco = await client.GetAsync();
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("likosto", "CrM75fnza%");
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
Console.WriteLine("\n\n");
//var newTokenResponse = await tokenClient.RequestRefreshTokenAsync(tokenResponse.RefreshToken);
// call api
var httpClient = new HttpClient();
httpClient.SetBearerToken(tokenResponse.AccessToken);
var response = await httpClient.GetAsync("http://localhost:21715/api/test");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
// HTTP StatusCode = 400 HERE <======================
}
else
{
var content = response.Content.ReadAsStringAsync().Result;
Console.WriteLine(JArray.Parse(content));
}
}

After looking at this more closely, it was because I was adding some very large data sets to my token as experimentation. IIS was sending the HTTP 400 because the request headers were too long.

Related

How to properly setup JwtBearerOptions

I setup Identity Server 4 to issue JWT tokens to authenticate users. In Identity Server 4 I have setup the following:
public class Resources {
public static IEnumerable<ApiResource> GetResources() {
return new[] {
new ApiResource {
Name = "Test.API",
DisplayName = "Test API",
Description = "Allow the user access to the test API",
Scopes = new List<string> { "Core API" },
UserClaims = new List<string> { "General", "Admin" }
}
};
}
public static IEnumerable<ApiScope> GetScopes() {
return new[] {
new ApiScope("Core.API", "Allow access to the test API")
};
}
public static IEnumerable<Client> GetClients() {
return new List<Client>() {
new Client {
ClientName = "Test Client",
ClientId = "b778a2ad-090d-4525-8954-6411de2cd339",
ClientSecrets = new List<Secret> { new Secret("random_text".Sha512()) },
AllowedScopes = new List<string> { "Core.API" },
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
},
new Client {
ClientName = "Test Web App",
ClientId = "abb9c89c-a018-4b0f-9a0f-4e701c637665",
ClientSecrets = new List<Secret> { new Secret("other_random_text".Sha512()) },
AllowedGrantTypes = GrantTypes.Hybrid,
RequirePkce = false,
AllowRememberConsent = false,
AllowedScopes = new List<string>
{
StandardScopes.OpenId,
StandardScopes.Profile,
StandardScopes.Address,
StandardScopes.Email,
"Core.API",
"roles"
}
}
};
}
public static IEnumerable<IdentityResource> GetIdentities() {
return new[] {
new IdentityResources.Email(),
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource {
Name = "User Role",
UserClaims = new List<string> { "Admin", "General" }
}
};
}
}
public class Startup {
public Startup(IConfiguration configuration, IWebHostEnvironment appEnv) {
Configuration = configuration;
CurrentEnvironment = appEnv;
}
public IConfiguration Configuration { get; }
private IWebHostEnvironment CurrentEnvironment { get; set; }
public void ConfigureServices(IServiceCollection services) {
services.AddScoped<IUserRequester, UserRequester>(_ =>
new UserRequester(Configuration.GetSection("AzureTableStore.UserLogin").Get<TableStoreConfiguration>()));
services.AddControllers().AddNewtonsoftJson(options =>
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());
IIdentityServerBuilder builder = services.AddIdentityServer();
if (CurrentEnvironment.IsDevelopment()) {
builder.AddDeveloperSigningCredential();
} else {
X509Certificate2 certData = DownloadCertificate(Configuration.GetSection("APICertificate").Get<Secret>());
builder.AddSigningCredential(certData);
}
builder.AddInMemoryClients(Resources.GetClients());
builder.AddInMemoryIdentityResources(Resources.GetIdentities());
builder.AddInMemoryApiResources(Resources.GetResources());
builder.AddInMemoryApiScopes(Resources.GetScopes());
builder.Services.Configure<TableStoreConfiguration>(Configuration.GetSection("AzureTableStore.UserLogin"));
builder.Services.Configure<RedisConfiguration>(Configuration.GetSection("RedisCache"));
builder.Services.AddTransient<IRedisConnection, RedisConnection>();
builder.Services.AddTransient<IUserRequester, UserRequester>();
builder.Services.AddTransient<IProfileService, ProfileService>();
builder.Services.AddTransient<IResourceOwnerPasswordValidator, PasswordValidator>();
builder.Services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
builder.Services.AddTransient<IReferenceTokenStore, ReferenceTokenStore>();
builder.Services.AddTransient<IRefreshTokenStore, RefreshTokenStore>();
builder.Services.AddTransient<IUserConsentStore, UserConsentStore>();
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new OpenApiInfo {
Version = "v1",
Title = "Authentication",
Description = "API allowing for user requests to be authenticated against their credentials",
Contact = new OpenApiContact {
Name = "Me",
Email = "me#fake.com"
}
});
string xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
string xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
services.AddCors(options => options.AddDefaultPolicy(
builder => builder.AllowAnyOrigin().
SetIsOriginAllowedToAllowWildcardSubdomains().
AllowAnyMethod().
AllowAnyHeader().
WithHeaders("X-TEST", "true")));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(c =>
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Authentication API v1"));
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}"));
}
private static X509Certificate2 DownloadCertificate(Secret secret) {
KeyVaultSecret secretValue = new Provider(secret.KeyVaultName).GetSecretAsync(secret.SecretName).Result;
var store = new Pkcs12Store();
using (Stream stream = secret.KeyVaultName.Equals("local")
? new FileStream(Environment.GetEnvironmentVariable(secret.SecretName), FileMode.Open)
: new MemoryStream(Convert.FromBase64String(secretValue.Value))) {
store.Load(stream, Array.Empty<char>());
}
string keyAlias = store.Aliases.Cast<string>().SingleOrDefault(a => store.IsKeyEntry(a));
var key = (RsaPrivateCrtKeyParameters)store.GetKey(keyAlias).Key;
var certificate = new X509Certificate2(
DotNetUtilities.ToX509Certificate(store.GetCertificate(keyAlias).Certificate));
var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(DotNetUtilities.ToRSAParameters(key));
return RSACertificateExtensions.CopyWithPrivateKey(certificate, rsa);
}
}
and in the Startup.cs of all of my services I have the following:
services.AddAuthentication(configuration.SchemeType).
AddJwtBearer("Bearer", options => {
options.Authority = "https://mytest.com/auth"; // Endpoint of the authentication service
options.TokenValidationParameters = new TokenValidationParameters {
ValidateAudience = false
};
});
// Ensure that the claim type is verified as well
services.AddAuthorization(options => options.AddPolicy("ClientIdPolicy", policy =>
policy.RequireClaim("client_id", "b778a2ad-090d-4525-8954-6411de2cd339", "abb9c89c-a018-4b0f-9a0f-4e701c637665")));
The problem I'm having is that this consistently fails. After trying to debug the issue, I've come to the realization that I don't really understand the purpose of this. Is it validating the fields on the JWT to ensure they're valid? If so, what value should I provide for Authority? Are there any other fields I need to set?
Update:
Upon further investigation, I see that requests return with a WWW-Authenticate response header that contains Bearer error="invalid_token", error_description="The signature key was not found". It appears that I've misconfigured either my Authentication service or my downstream services but I'm not sure which.
Thanks to the article provided by #MichalTrojanowski as well as this post, I was able to determine that there were two problems with how I was authenticating JWTs:
I had my authority set to the wrong value. Or rather, my value for Authority matched the actual endpoint for authorizing tokens but that value did not match what was printed in /.well-known/openid-configuration. Therefore, the authentication failed.
My issuer did not match the iss value in the JWT.
After fixing these two problems, my services have been able to authenticate properly.

How to implement authorize web api using OAuth clientID and secret

I want to implement OAuth authorization on web api. I am generating JWT token using microsoft graph api
public Token GetToken()
{
try
{
string baseAddress = "https://login.windows.net/<tenantid>/oauth2/token?api-version=1.0";
var client = new HttpClient();
var form = new Dictionary<string, string>{
{
"grant_type",
"client_credentials"
},
{
"client_id",
"<clientid>"
},
{
"client_secret",
"<clientsecret>"
}
};
var tokenResponse = client.PostAsync(baseAddress, new FormUrlEncodedContent(form)).Result;
var token = tokenResponse.Content.ReadAsAsync<Token>(new[] { new JsonMediaTypeFormatter() }).Result;
return token;
}
catch (Exception exc)
{
return new Token();
}
}
I am getting back jwt token but my controller is not able to authorize this. My Startup.cs looks like this:
public void Configuration(IAppBuilder app)
{
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
ConfigureAuth(app);
}
public void ConfigureAuth(IAppBuilder app)
{
var OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new PathString("/oauth2/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(60), //token expiration time
Provider = new OAuthProvider(),
};
app.UseOAuthBearerAuthentication(OAuthBearerOptions);
app.UseOAuthAuthorizationServer(oAuthServerOptions);
}

IdentityServer4 Resource owner password and Win auth: unauthorized

in the last few days I've been reading IdentityServer4 docs and putting together my sample server + sample client using Resource owner password. Now I'd like to add Windows authentication (will be done via Active Directory) in parallel, so the client app (not a web app but a desktop app) could either prompt the user for credentials or login using Windows authentication via Active Directory.
The documentation about Windows Authentication explains how to configure IIS or HTTP.Sys, but what I want is to:
user opens the app
the app use single sign on to post a request to the web api to request token and refresh token
the web api uses windows authentication to validate the identity of the user and returns the token
I've tried to follow this answer, but it doesn't work (It returns unauthorized).
web api: Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
// base-address of your identityserver
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
// name of the API resource
options.Audience = "api/user";
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddIdentityServer(options => { options.PublicOrigin = "http://localhost:5000"; options.MutualTls.Enabled = false; })
.AddExtensionGrantValidator<WinAuthGrantValidator>()
.AddDeveloperSigningCredential()
.AddTestUsers(Config.GetUsers())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddInMemoryIdentityResources(Config.GetIdentityResources());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILogger<Startup> logger, IServer server)
{
app.Use(async (context, next) =>
{
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
.MaxRequestBodySize = 10 * 1024;
var serverAddressesFeature =
app.ServerFeatures.Get<IServerAddressesFeature>();
var addresses = string.Join(", ", serverAddressesFeature?.Addresses);
logger.LogInformation($"Addresses: {addresses}");
await next.Invoke();
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// Enable HTTPS Redirection Middleware when hosting the app securely.
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
app.UseIdentityServer();
app.UseAuthentication();
}
}
internal static class Config
{
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password"
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password"
}
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
// other clients omitted...
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
// AllowedScopes = { "api1" }
AllowedScopes = { "api/user" }
},
new Client
{
ClientId = "winauth",
AllowedGrantTypes = new List<string>{ "windows_auth" },
ClientSecrets =
{
new Secret("secret".Sha256())
},
// AllowedScopes = { "api1" }
AllowedScopes = { "api/user" }
}
};
}
internal static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource { Name = "api1",Scopes = new List<Scope> { new Scope { Name = "api1",
DisplayName = "Full access to API 2"} }, Enabled = true, ApiSecrets = new List<Secret>
{
new Secret("secret".Sha256())
}
},
new ApiResource { Name = "api/user",Scopes = new List<Scope> { new Scope { Name = "api/user",
DisplayName = "Full access to API 2"} }, Enabled = true, ApiSecrets = new List<Secret>
{
new Secret("secret".Sha256())
}
}};
}
public static List<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
}
public class WinAuthGrantValidator : IExtensionGrantValidator
{
private readonly HttpContext httpContext;
public string GrantType => "windows_auth";
public WinAuthGrantValidator(IHttpContextAccessor httpContextAccessor)
{
httpContext = httpContextAccessor.HttpContext;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
// see if windows auth has already been requested and succeeded
var result = await httpContext.AuthenticateAsync("Windows");
if (result?.Principal is WindowsPrincipal wp)
{
context.Result = new GrantValidationResult(wp.Identity.Name, GrantType, wp.Claims);
}
else
{
// trigger windows auth
await httpContext.ChallengeAsync("Windows");
context.Result = new GrantValidationResult { IsError = false, Error = null, Subject = null };
}
}
}
}
web API: Program.cs
public class Program
{
public static void Main(string[] args)
{
var isService = !(Debugger.IsAttached || args.Contains("--console"));
if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);
Directory.SetCurrentDirectory(pathToContentRoot);
}
var builder = CreateWebHostBuilder(
args.Where(arg => arg != "--console").ToArray());
var host = builder.Build();
if (isService)
{
// To run the app without the CustomWebHostService change the
// next line to host.RunAsService();
host.RunAsCustomService();
}
else
{
host.Run();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddEventLog();
})
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here.
})
.UseStartup<Startup>()
.UseHttpSys(options =>
{
options.AllowSynchronousIO = true;
options.Authentication.Schemes = Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes.Kerberos | Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes.NTLM;
options.Authentication.AllowAnonymous = true;
options.MaxConnections = null;
options.MaxRequestBodySize = 30000000;
options.UrlPrefixes.Add("http://localhost:5000");
});
}
web API UserController.cs
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Bearer")]
[ApiController]
public class UserController : ControllerBase
{
// GET api/user
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { User.Identity.Name, User.Identity.AuthenticationType };
}
}
client code:
using (var client = new HttpClient())
{
disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
{
Address = baseUrl,
Policy = { RequireHttps = false }
});
if (disco.IsError)
{
Console.WriteLine(disco.Error);
Console.ReadLine();
return;
}
var httpHandler = new HttpClientHandler
{
UseDefaultCredentials = true,
};
using (var client = new HttpClient())
{
// request token
TokenResponse tokenResponse = await client.RequestTokenAsync(new TokenRequest
{
GrantType = "windows_auth",
Address = disco.TokenEndpoint,
ClientId = "winauth",
ClientSecret = "secret"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
Console.ReadLine();
return;
}
}
It returns unauthorized.
I found a solution to this: I need to configure a TestUser with SubjectId = MYDOMAIN\myusername then it worked.
The error was getting was overly confusing.

PasswordTokenRequest returning invalid_client

I am attempting to get a token from a .net core api controller using a password with identity server 4. I am receiving Error invalid_client.
here is the controller.
[HttpGet]
public async Task<IActionResult> Get()
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:44321");
var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "htmlClient",
ClientSecret = "secretpassword",
UserName = "someguy#gmail.com",
Password = "password",
Scope = "WebApi.ReadAccess"
});
return Ok();
}
here is the config
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource(
"WebApi.ReadAccess",
"WebApi API",
new List<string> {
JwtClaimTypes.Id,
JwtClaimTypes.Email,
JwtClaimTypes.Name,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName
}
),
new ApiResource("WebApi.FullAccess", "WebApi API")
};
}
public static IEnumerable<Client> GetClients()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "HTML Page Client",
ClientId = "htmlClient",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secretpassword")
},
AllowedScopes = { "WebApi.ReadAccess" }
}
};
}
}
in startup.cs in configureServices
services.AddIdentityServer()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddProfileService<ProfileService>()
.AddDeveloperSigningCredential();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = "https://localhost:44321";
o.Audience = "WebApi.ReadAccess";
o.RequireHttpsMetadata = false;
});
and in configure I have app.UseIdentityServer();
public void Configure(IApplicationBuilder app, IHostingEnvironment env, BooksContext booksContext)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseIdentityServer();
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(options =>
options.SwaggerEndpoint("/swagger/v2/swagger.json", "Book Chapter Service"));
app.UseDefaultFiles();
app.UseStaticFiles();
}
Try changing your code to following. I have given the generic code, you can change it according to your need.
return new List<ApiResource>
{
new ApiResource
{
Name = "api",
DisplayName = "WebApi API",
Scopes =
{
new Scope("WebApi.ReadAccess", "Read write access to web api")
}
},
new ApiResource
{
Name = "api",
DisplayName = "WebApi API",
Scopes =
{
new Scope("WebApi.FullAccess", "Full access to web api")
}
}
}
and
o.Audience = "api";
The reason being,
Your o.Audience name should match ApiResource.Name because it indicates mapping between your authority and audience.
For example in your case Authority https://localhost:44321 has audience lets say called "api".
"api" also is a name of your ApiResource which is giving authority to create access token.
Hope this helps!

Token from identity server connection\token is not valid for my API

I am getting a token from my identityserver 4 via url connection/token with a POST request:
Then I copy/paste the value of the access_token key to my API GET request as a header:
mytokenstring
eyJhbGciOiJSUzI1NiIsImtpZCI6IkYxMDhCODA2NUNFMTRBOEEwOTZBODUyMkIxQUNBMkFDMTdEQjQwNEEiLCJ0eXAiOiJKV1QiLCJ4NXQiOiI4UWk0Qmx6aFNvb0phb1Vpc2F5aXJCZmJRRW8ifQ.eyJuYmYiOjE1MDg1OTU5MzIsImV4cCI6MTUwODU5OTUzMiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo2NTUzNSIsImF1ZCI6WyJodHRwOi8vbG9jYWxob3N0OjY1NTM1L3Jlc291cmNlcyIsInRlYWNoZXJzX3Rlc3RfcGxhbm5lciJdLCJjbGllbnRfaWQiOiJ0ZWFjaGVyc190ZXN0X3BsYW5uZXIiLCJzY29wZSI6WyJ0ZWFjaGVyc190ZXN0X3BsYW5uZXIiXX0.g2x31JcYrXyIavfxCu7UKY3kndznI_gYHJYCxl0dQn3u7l7vWo6qKr13XYMo6P1Lqtu68T2FEXL-5kyS0XwFClpdJE6m13-hfKZsd2QHBmOlgZ2ANwghXW4hfU5nWiwkUACwkP9wfDCULV3oQm5i49L5TQmUiiqcy0TTS2FDBdS5ymFBi1bCKnPh5ErsD8V_4eTqLzxv8CyVkPx2gPd6aBIf_2JNrjrMrrm69kghOHnktVG17KPQhppbIeJO8RP-URiJUJGXIY09yRGVF7YXtkFj-I5QOMvNIAWgUeqNYqH0cuQol9nglA4mtU1MfXtnRoEpRRzGViw7gxJ_-MFadA
Authorization: Bearer mytokenstring
What can cause that the token from the identity server is not valid for my API?
I get a 401 error with POSTMAN
Looking into the output of the kestrel server I get this:
Api> fail: Microsoft.AspNetCore.Server.IISIntegration.IISMiddleware[0]
Api> 'MS-ASPNETCORE-TOKEN' does not match the expected pairing token '52da49ee-6599-483a-b97a-15ced1603005', request rejected.
What do I wrong and what pairing token Guid is that?
API HttpGet:
header:
Authorization Bearer eyJh...UntilTheEndOfTheString
IdentityServer setup:
public void ConfigureServices(IServiceCollection services)
{
string certificateFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certifiateselfsigned.pfx");
var certificate = new X509Certificate2(certificateFilePath, "test");
services.AddIdentityServer()
.AddSigningCredential(certificate)
.AddInMemoryApiResources(InMemoryConfiguration.GetApiResources())
.AddInMemoryClients(InMemoryConfiguration.GetClients())
.AddTestUsers(InMemoryConfiguration.GetUsers());
services.AddMvc();
}
UPDATE
Api
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddIdentityServerAuthentication(opt =>
{
opt.RequireHttpsMetadata = false;
opt.Authority = "http://localhost:65535"; // IdentityProvider => port running IDP on
opt.ApiName = "teachers_test_planner"; // IdentityProvider => api resource name
});
services.AddMvc();
}
IdentityProvider
public static class InMemoryConfiguration
{
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser{ SubjectId = "6ed26693-b0a1-497e-aa14-7b880536920f", Username = "orders.tatum#gmail.com", Password = "mypassword",
Claims = new List<Claim>
{
new Claim("family_name", "tatum")
}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("teachers_test_planner", "Mein Testplaner")
};
}
public static IEnumerable<IdentityResource> GetIdentyResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "teachers_test_planner",
ClientSecrets = new[]{ new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
AllowedScopes = new []{ "teachers_test_planner" }
}
};
}
}
UPDATE 2
You can find the test project here:
https://github.com/LisaTatum/IdentityServer4Test
UPDATE 3
As nobody asked how I do the http_post to the connect/token endpoint here it is:
All you missed is app.UseAuthentication(). You have to add this into Configure method on Api startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(); // Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseAuthentication();// The missing line
app.UseStaticFiles();
app.UseMvc();
}
I write the following consle app to test calling your api
class Program
{
public static void Main(string[] args) => MainAsync().GetAwaiter().GetResult();
private static async Task MainAsync()
{
// discover endpoints from metadata
var disco = await DiscoveryClient.GetAsync("http://localhost:65535");
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "teachers_test_client", "secret");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("teachers_test_planner");
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
Console.WriteLine("\n\n");
// call api
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var response = await client.GetAsync("http://localhost:52129/api/values/1");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
}
Console.ReadKey();
}
}
You could try on the postman, i haven't tried that
However, I had to change few more things on your project(github) to make it run on my machine. And they are
Project: IdentityProvider
mytestplanner.pfx was not set to Copy Always
Project: Api2
uninstall IdentityServer4 package. Why do you need IdentityServer4 on the client?
Change target framework from <TargetFramework>net462</TargetFramework> to <TargetFramework>netcoreapp2.0</TargetFramework>, (the reason is that 4.6.2 is not installed on my machine)
Anyway, let me know if this works for you
Update
I have added the working project here
This can happen if you request a "wrong" token.
Assuming you have Client C calling API A, "POST GetToken" would look something like this:
ClientId: C.ClientId
Resource: A.Resource ("audience")
Your token suggests that you are doing the following request
ClientId: teachers_test_planner
Resource/Audience: "http://localhost:65535/resources" & "teachers_test_planner"
Is "http://localhost:65535/resources" the audience of your target API?

Categories