IdentityServer4 Resource owner password and Win auth: unauthorized - c#

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.

Related

c# Web API 500 error from method after adding authentication

I have been trying to fix this all day. I am making a test API to practice my development. I tried adding a bearer authentication error and now none of the methods work.
namespace WebApiTest
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(loggingBuilder =>
{
loggingBuilder.AddConsole();
});
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header. \r\n\r\n Enter the token in the text input below."
});
c.OperationFilter<AddAuthorizationHeaderParameterOperationFilter>();
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
try
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Web API Test");
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while configuring the application.");
throw;
}
}
}
public class AddAuthorizationHeaderParameterOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var authAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType<AuthorizeAttribute>();
if (authAttributes.Any())
{
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" },
},
new string[] {}
}
}
};
}
}
}
}
namespace AzureWebApiTest.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class MainController : ControllerBase
{
private readonly LoginRequest loginInformation = new LoginRequest("username", "password");
[HttpPost("GetToken")]
public IActionResult GetToken([FromBody] LoginRequest loginRequest)
{
if (loginRequest == null)
{
return BadRequest("Bad Login Request");
}
if (loginRequest.Equals(loginInformation))
{
var token = GenerateBearerToken(loginRequest);
return Ok(new { token });
}
else
{
return Unauthorized("Incorrect Login Information");
}
}
[HttpGet("GetHello")]
[Authorize(AuthenticationSchemes = "Bearer")]
public IActionResult GetHello([FromQuery] string name)
{
try
{
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
var tokenHandler = new JwtSecurityTokenHandler();
var tokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("secretKey"))
};
var claimsPrincipal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
return Ok("Hello " + name);
}
catch (SecurityTokenExpiredException)
{
return Unauthorized("Token has expired.");
}
catch (SecurityTokenInvalidSignatureException)
{
return Unauthorized("Invalid token signature.");
}
catch (Exception)
{
return Unauthorized("Invalid token.");
}
}
private string GenerateBearerToken(LoginRequest loginRequest)
{
if (ValidateCredentials(loginRequest))
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("secretKey");
var tokenDescriptor = new SecurityTokenDescriptor
{
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
return null;
}
private bool ValidateCredentials(LoginRequest loginRequest)
{
if(loginRequest.Equals(loginInformation))
{
return true;
}
return false;
}
}
}
When I try the method in swagger, I am getting the response:
I've tried getting ChatGPT to fix it but I'm getting nowhere and it's going in circles. Anyone have any ideas?
Edit:
The Validate function returns false at the end, I changed it for testing purposes. Edited back.
You are trying to get authorization to work, but you lack a few things.
Authentication
services.AddAuthentication(..options..).AddJwtBearer(..options..)
You need to add authentication to the pipeline, by adding UseAuthentication() before UseAuthorization(), like:
app.UseAuthentication();
app.UseAuthorization();
You need to add / register the authorization service using
services.AddAuthorization(..options..);

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.

IdentityServer4 and Code with PKCE testing with Postman

I'm getting started with IdentityServer4 and I made it through with resource owner flow, but since it's not recommended anymore, PKCE is, I decided to change it. I'm getting the following error message which is obvious because I'm no longer using GrantTypes.ResourceOwnerPassword.
fail: IdentityServer4.Validation.TokenRequestValidator[0]
Client not authorized for resource owner flow, check the AllowedGrantTypes setting{ client_id = trusted }, details: {
"ClientId": "trusted",
"ClientName": "Dayum Client",
"GrantType": "password",
"Raw": {
"grant_type": "password",
"username": "Admin",
"password": "***REDACTED***",
"scope": "openid profile offline_access api1",
"client_id": "trusted"
}
}
I don't find much information, because it's new but how am I supposed to test it with Postman? I used to do the following with resource owner flow:
POST http://localhost:58508/connect/token
grant_type = password
username=Admin
password=123456
scope=openid profile offline_access api1
client_id=trusted
I know I can't do that anymore with Code with PKCE. How do I request an access and refresh tokens, how do I test it with Postman?
Code:
public static class Config
{
public static IEnumerable<IdentityResource> GetResources() =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
public static IEnumerable<ApiResource> GetApis() =>
new List<ApiResource>
{
new ApiResource("api1", "My API")
};
public static IEnumerable<Client> GetClients() =>
new List<Client>
{
new Client
{
ClientId = "trusted",
ClientName = "Dayum Client",
//ClientSecrets = { new Secret("xxxxxxxxxxxxxxxxxxxxxxx".Sha256()) },
RequireConsent = false,
RequireClientSecret = false,
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
AllowAccessTokensViaBrowser = true,
RedirectUris = { "http://localhost:58508" },
PostLogoutRedirectUris = { "http://localhost:58508" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"api1"
},
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 900,
AllowOfflineAccess = true,
RefreshTokenExpiration = TokenExpiration.Absolute,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
AbsoluteRefreshTokenLifetime = 1800
}
};
}
public class Startup
{
public IWebHostEnvironment Environment { get; }
public IConfiguration Configuration { get; }
public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
Environment = environment;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration["ConnectionStrings:DayumConnection"],
optionsBuilder => optionsBuilder.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddSigningCredential(new X509Certificate2(Configuration["Certificates:Default:Path"], Configuration["Certificates:Default:Password"]))
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration["ConnectionStrings:DayumConnection"],
optionsBuilder => optionsBuilder.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration["ConnectionStrings:DayumConnection"],
optionsBuilder => optionsBuilder.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName));
options.EnableTokenCleanup = true;
})
.AddProfileService<ProfileService>()
.AddAspNetIdentity<ApplicationUser>();
}
public void Configure(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
context.Database.Migrate();
if (!context.Clients.Any())
{
foreach (var client in Config.GetClients())
{
context.Clients.Add(client.ToEntity());
}
context.SaveChanges();
}
if (!context.IdentityResources.Any())
{
foreach (var resource in Config.GetResources())
{
context.IdentityResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
if (!context.ApiResources.Any())
{
foreach (var resource in Config.GetApis())
{
context.ApiResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
}
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseIdentityServer();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "MyArea",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
The first thing is that PKCE support for OAuth 2.0 is available in the latest version of Postman app (v7.23.0) , please update your Postman to the latest version .
Then in Authorization header of Postman , set Type to OAuth2 and click the Get New Access Token button , set Grant Type to Authorization code(With PKCE) and set endpoints/client info as below :
And Auth url/Access Token Url is your identity server 4 's endpoints and replace the Callback url as your client app's redirect url . I notice in your codes , you set the same endpoint/url host(http://localhost:58508) in identity server and client , please modify that based on your real requirements .

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!

IdentityServer4 with Resource API in .NET 4.5 (OWIN)

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.

Categories