openiddict with Owin - Unable to obtain configuration from .well-known/openid-configuration - webforms

I've been trying an OpenID authorization code flow using openiddict. Both the OIDC client and server are implemented using OpenIddict (with Owin) and both are web forms apps hosted in IIS.
The client side config:
public bool SetUp(IAppBuilder app)
{
try
{
var container = CreateContainer();
app.UseAutofacLifetimeScopeInjector(container);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieHttpOnly = true,
CookieSecure = CookieSecureOption.SameAsRequest
});
app.UseMiddlewareFromContainer<OpenIddictClientOwinMiddleware>();
DependencyResolver.SetResolver(new
AutofacDependencyResolver(container));
var scope = container.BeginLifetimeScope();
return true;
}
catch (Exception ex)
{
_logger.Error("OidcController: SetUp", ex);
return false;
}
}
private IContainer CreateContainer()
{
IdentityModelEventSource.ShowPII = true;
var services = new ServiceCollection();
services
.AddOpenIddict()
.AddCore(options => { })
.AddClient(options => {
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate()
.UseOwin()
.EnableRedirectionEndpointPassthrough();
options.UseSystemNetHttp();
//Try to reuse key cloak options
options.AddRegistration(new OpenIddictClientRegistration
{
Issuer = new Uri(_oidcSettings.ExternalUrl.RemoveAuth(), UriKind.Absolute),
ClientId = _oidcSettings.ClientId,
ClientSecret = _oidcSettings.ClientSecret,
RedirectUri = <the redirect uri>,
Scopes = { "email", "profile", "offline_access" }
});
});
var builder = new ContainerBuilder();
builder.Populate(services);
builder.RegisterControllers(typeof(OidcController).Assembly);
return builder.Build();
}
The server config:
protected void Application_Start(object sender, EventArgs e)
{
// Code that runs on application startup
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
var services = new ServiceCollection();
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFramework()
.UseDbContext<ApplicationDbContext>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the authorization and token endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token");
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles);
options.AllowAuthorizationCodeFlow();
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseOwin()
.EnableAuthorizationEndpointPassthrough();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
options.UseLocalServer();
// Register the OWIN host.
options.UseOwin();
});
// Create a new Autofac container and import the OpenIddict services.
var builder = new ContainerBuilder();
builder.Populate(services);
Provider = new ContainerProvider(builder.Build());
Task.Run(async delegate
{
using var scope = Provider.ApplicationContainer.BeginLifetimeScope();
var context = scope.Resolve<ApplicationDbContext>();
context.Database.CreateIfNotExists();
var manager = scope.Resolve<IOpenIddictApplicationManager>();
//Add the client app
if (await manager.FindByClientIdAsync("SAS-Console") == null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "SAS-Console",
ClientSecret = "413ca0fd-8e24-4362-aaa3-d74095227658",
ConsentType = ConsentTypes.Implicit,
DisplayName = "Console application",
RedirectUris =
{
<client app redirect url>
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.ResponseTypes.Code
}
});
}
}).GetAwaiter().GetResult();
}
But when I run both, I continue to get an error:
IDX20803: Unable to obtain configuration from: 'https:///.well-known/openid-configuration

Related

identity server 4 Logout from all clients using FrontChannelLogoutUri not working

Here I want to achieve the SSO feature. if I logout from Identity server all clients connected to that with that userid should be logged out. But it is working for me.
I am using Identity Server Version="4.0.0"
I have setup the IDS clients , UI templates for loggedout, MVC client as below. But it is not signout from all clients.
new Client
{
ClientId = "testmvc",
AllowedGrantTypes = GrantTypes.Code,
// secret for authentication
ClientSecrets =
{
new Secret("Secret".Sha256())
},
AllowOfflineAccess = true,
// scopes that client has access to
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Address,
"roles"
},
// 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" },
FrontChannelLogoutUri = "https://localhost:5002/home/frontchannellogout" ,
}
In MVC client I have created two logouts
//UI Logout
public async Task<IActionResult> Logout()
{
var client = _httpClientFactory.CreateClient("IDPClient");
var discoveryDocumentResponse = await client.GetDiscoveryDocumentAsync();
if (discoveryDocumentResponse.IsError)
{
throw new Exception(discoveryDocumentResponse.Error);
}
var accessTokenRevocationResponse = await client.RevokeTokenAsync(
new TokenRevocationRequest
{
Address = discoveryDocumentResponse.RevocationEndpoint,
ClientId = "testmvc",
ClientSecret = "Secret",
Token = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken)
});
if (accessTokenRevocationResponse.IsError)
{
throw new Exception(accessTokenRevocationResponse.Error);
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
return Redirect(discoveryDocumentResponse.EndSessionEndpoint);
}
//Front channel logout
public async Task<IActionResult> FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
var currentSid = User.FindFirst("sid")?.Value ?? "";
if (string.Equals(currentSid, sid, StringComparison.Ordinal))
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
return NoContent();
}
In the Logout method, you are signing out from both auth schemes (Cookies and OIDC), whereas in FrontChannelLogout you seem to be signing out only from the Cookies scheme. Isn't that the problem?
Either way, please try to define the methods like below, and check if the corresponding OIDC logout endpoint is called.
public async Task Logout()
{
// ...
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
public async Task FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
//...
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
}

send custom object in response when jwt authentication fails in asp.net web api

I have created a ASP.NET WEB.API project with OWIN JWT Authentication. Is is working as expected, but what I want is to show custom validation message to use instead of default message if JWT authentication fails.
Right now it shows
{
"message": "Authorization has been denied for this request."
}
This is my Validation Code.
public class OwinStartUp
{
public void Configuration(IAppBuilder app)
{
HttpStatusCode statusCode;
HttpResponseMessage message = new HttpResponseMessage();
try
{
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "http://jwt.com",
ValidAudience = "http://jwt.com",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("jwt_key_asdf"))
}
});
}
catch (SecurityTokenValidationException e )
{
statusCode = HttpStatusCode.Unauthorized;
message = new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Content = new StringContent("Failed") /////// Even tried this but it does not work.
};
}
catch(Exception ex)
{
}
}
}

When I make a POST to my Web API I get a 401 exception

I am making an Ajax call to a Web Core API server using localhost on my development machine.
This is the code that makes the call;
addDataToDatabase = function (callback, errorCallback, url, data) {
$.ajax({
async: true,
url: url,
contentType: "application/json",
dataType: "text",
data: data,
type: "POST",
xhrFields: { withCredentials: true }
})
.done(function (data) {
callback(data);
})
.fail(function (data) {
errorCallback(data);
});
However I get a 401 unauthorised user exception.
In my startup Configure method I set up CORS as follows
var url = Configuration["originUrl"];
app.UseCors(
options => options.WithOrigins(url).AllowAnyHeader().AllowAnyMethod().AllowCredentials()
);
In my appSettings file I set the orginUrl:
"originUrl": "http://localhost:12345"
Which all works for GET type calls but not POST.
What do I need to fix?
EDIT - upon request this is the full startup code;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
namespace Properties.API
{
using Domain;
using Domain.Interfaces;
using Domain.Repo;
using EF6;
using Helper;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog;
using NLog.Config;
using NLog.Extensions.Logging;
using NLog.Web;
using Sir.EF6;
using Sir.EF6.Interfaces;
using Sir.EF6.Repo;
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appSettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appSettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public static IConfigurationRoot Configuration;
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new AssemblyPart(typeof(Startup).Assembly));
services.AddSingleton(manager);
services.AddCors();
services.AddMvcCore().AddJsonFormatters();
services.Configure<IISOptions>(options => new IISOptions
{
AutomaticAuthentication = true,
ForwardClientCertificate = false,
ForwardWindowsAuthentication = false
});
var connectionStringMSurveyV2 = Configuration.GetConnectionString("MSurveyV2Db");
services.AddScoped<MSurveyV2Db>(_ => new MSurveyV2Db(connectionStringMSurveyV2));
var connectionStringSir = Configuration.GetConnectionString("SirDb");
services.AddScoped<SirDb>(_ => new SirDb(connectionStringSir));
services.AddScoped<IPropertiesRepo, PropertiesRepo>();
services.AddScoped<ISirUoW, SirUoW>();
services.AddScoped<IPropertyUoW, PropertyUoW>();
services.AddScoped<Services.IMailService, Services.MailService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
loggerFactory.AddDebug();
loggerFactory.AddNLog();
LogManager.Configuration = new XmlLoggingConfiguration(#"nlog.config");
var connectionString = Configuration.GetConnectionString("SirNLogDb");
LogManager.Configuration.Variables["SirNLogDb"] = connectionString;
app.AddNLogWeb();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler();
}
var url = Configuration["originUrl"];
app.UseCors(
options => options.WithOrigins(url).AllowAnyHeader().AllowAnyMethod().AllowCredentials()
);
app.UseMvc();
AutomapperInit.Set();
var logger = LogManager.GetCurrentClassLogger();
logger.Info("Started Properties Web API");
logger.Info($"Origin url = {url}");
}
}
}
This is a POST method that provokes the 401 exception;
[HttpPost("add")]
public async Task<IActionResult> Add([FromBody] InspectionVisitInputDto inspectionVisitDto)
{
this.NLogger.Info("api/inspectionVisit/add".ToPrefix());
if (inspectionVisitDto == null)
{
NLogger.Error("No data sent");
return BadRequest(ModelState);
}
if (inspectionVisitDto.InspectionId < 1)
{
NLogger.Error($"Invalid InspectionId < 1 (actual value is {inspectionVisitDto.InspectionId}");
return BadRequest(ModelState);
}
var inspectionVisit = Mapper.Map<InspectionVisit>(inspectionVisitDto);
var dateOfVisit = inspectionVisit.DateOfVisit.Date;
try
{
var existingInspectionVisit = this.SirUoW.InspectionVisit.GetItem(
x => x.InspectionId == inspectionVisit.InspectionId &&
DbFunctions.TruncateTime(x.DateOfVisit) == dateOfVisit &&
x.ContractSubcontractorId == inspectionVisit.ContractSubcontractorId &&
x.SelectedInspectorUsername == inspectionVisit.SelectedInspectorUsername &&
x.WorkPhaseId == inspectionVisit.WorkPhaseId);
if (existingInspectionVisit?.InspectionVisitId > 0)
{
NLogger.Info($"Inspection Visit Id = {existingInspectionVisit.InspectionVisitId} already exists.");
return Ok(existingInspectionVisit.InspectionVisitId);
}
}
catch (Exception e)
{
var message = "Cannot get inspection visit";
await ReportException(e, message);
var status = OperationStatus.CreateFromException(message, e);
return BadRequest(status);
}
NLogger.Info("Add New Inspection Visit");
try
{
this.SirUoW.InspectionVisit.Add(inspectionVisit);
await SirUoW.LoggedInSaveChangesAsync(this.LoggedInUser);
NLogger.Info("New Inspection Visit Saved, id = " + inspectionVisit.InspectionVisitId);
if (inspectionVisit.ContractSubcontractorId != null)
{
await SaveContractSubcontractorIdForInspection(inspectionVisitDto);
}
return Ok(inspectionVisit.InspectionVisitId);
}
catch (Exception e)
{
var message = "Cannot save " + inspectionVisitDto;
await ReportException(e, message);
var status = OperationStatus.CreateFromException(message, e);
return BadRequest(status);
}
}
And this is a GET method that works OK
[HttpGet("getall")]
public async Task<IActionResult> GetContracts()
{
this.NLogger.Info("api/contracts/getall".ToPrefix());
try
{
var contracts = this.SirUoW.ViewAllContracts.GetList(x => x.IsMainDivision && x.EndDate == null).OrderBy(x => x.FullContractNo).ToList();
return Ok(contracts);
}
catch (Exception e)
{
var message = "Error getting contracts";
await ReportException(e, message);
var status = OperationStatus.CreateFromException(message, e);
return BadRequest(status);
}
}
Your scenario is combining IIS Windows Authenticaiton and CORS, since preflight request OPTIONS would not contains any security header, it will hit the error 401.
Try workaround below:
Enable anonymousAuthentication in appsettings.json
{
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": true
}
}
For enable authentication, try Filter to disable anonymous access.
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});

which TokenValidationParameters are needed with UseJwtBearerAuthentication

I am tying to do JwtBearerAuthentication on my .net WebAPI and it is just not working. The Authorize attribute is always claiming isAuthorized = false.
I am working with Okta as the SSO. I am authenticating on my client side and getting both an access token and id token. On a webapi get request I am providing the access token (i have also tried the id token) in the authorize header and I am able to see the authorize header with the token in the webapi actioncontext.
In my startup.cs I have the following
var clientID = WebConfigurationManager.AppSettings["okta:ClientId"];
var oidcIssuer = WebConfigurationManager.AppSettings["okta:OIDC_Issuer"];
TokenValidationParameters tvps = new TokenValidationParameters
{
ValidAudience = clientID,
ValidateAudience = true,
ValidIssuer = oidcIssuer,
ValidateIssuer = true
};
app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
{
TokenValidationParameters = tvps,
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new OpenIdConnectCachingSecurityTokenProvider(oidcIssuer + "/.well-known/openid-configuration")
}
});
Am i missing some TokenValidationParameters that I need?
As I was typing this, I see that twaldron was able to figure it out!
I also realized that he was asking about WebAPI, and not MVC. However, here is the code that I needed to get the following working with ASP.NET Core MVC, of particular interest might be this line, which is necessary to get access to the additional claims in the JWT:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
Here is how this code sample works from the command line, the $ID_TOKEN variable contains a valid JWT:
$ curl -H "Authorization: Bearer ${ID_TOKEN}" http://localhost:3000/test/test
sub: 01a23b4cd5eFgHI6j7k8 email:test#example.com
Setup.cs:
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
namespace WebApplication
{
public class Startup
{
readonly string clientId = string.Empty;
readonly string issuer = string.Empty;
readonly string audience = string.Empty;
public Startup(IHostingEnvironment env)
{
clientId = "A0b1CDef2GHIj3k4lm5n";
issuer = "https://example.okta.com";
audience = "A0b1CDef2GHIj3k4lm5n";
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddDebug();
// https://github.com/aspnet/Security/issues/1043#issuecomment-261937401
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
TokenValidationParameters tvps = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = audience,
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
MetadataAddress = issuer + "/.well-known/openid-configuration",
TokenValidationParameters = tvps
});
app.UseStaticFiles();
// Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715
app.UseMvc(routes =>
{
routes.MapRoute(
name: "test-controller",
template: "test/{action}",
defaults: new { controller = "Test", action = "Index" }
);
routes.MapRoute(
name: "default",
template: "{controller=Test}/{action=Index}/{id?}");
});
}
}
}
In Controllers/Test.cs:
[Authorize]
public IActionResult Test()
{
var contextUser = User.Identity as ClaimsIdentity;
Dictionary<string, string> claim = contextUser.Claims.ToDictionary(x => x.Type, x => x.Value);
var output = "sub: " + claim["sub"] + " email:" + claim["email"];
return Content(output);
}
My problem was not with the options.
It was 100% the need to move
app.UseWebApi(config);
below all the owin setup stuff.

Can we use OAuth to authenticate the consumers in my Web API?

I am developing a web application, mobile application and desktop application which all can access the data with the help of a single API which can be developed by ASP.NET Web API.
In my Web API can I authenticate the user credentials and the consumer Application key with the help of OAuth?
Can you guys guide me to achieve the same with any examples?
Add the following lines in startup class
public class Startup
{
public void Configuration(IAppBuilder app)
{
var oauthProvider = new OAuthAuthorizationServerProvider
{
OnGrantResourceOwnerCredentials = async context =>
{
IsValid = true;
//You can get the username and password by context.username and context.password
if (IsValid)
{
var claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
claimsIdentity.AddClaim(new Claim("user", context.UserName));
context.Validated(claimsIdentity);
return;
}
context.Rejected();
},
OnValidateClientAuthentication = async context =>
{
string clientId;
string clientSecret;
if (context.TryGetBasicCredentials(out clientId, out clientSecret))
{
if (clientId == GlobalAppSettings.SystemSettings.ApplicationKey)
{
context.Validated();
}
}
}
};
var oauthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/accesstoken"),
Provider = oauthProvider,
AuthorizationCodeExpireTimeSpan = TimeSpan.FromMinutes(1),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(3),
SystemClock = new SystemClock()
};
app.UseOAuthAuthorizationServer(oauthOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
And invoke the startup method by start up the new OAuth APP from where you need the authorization and OAuth token
You can use your own host by using the self hosting namespace.
Otherwise you can use Microsoft.Owin.Host.HttpListener namespace and host the OAuth app in different host as below
var Basesite = "http://localhost:9327/";
var homeProcessorModel = new HomeProcessorModel();
using (WebApp.Start<Startup>(url: Basesite))
{
var client = new HttpClient();
var form = new Dictionary<string, string>
{
{"grant_type","password"},
{"userName",username},
{"passWord",password}
};//If you are using grant_type as password, you have to send the username and password to OAuth protocol.
var tokenResponse = client.PostAsync(Basesite + "accesstoken", new FormUrlEncodedContent(form)).Result;
var token = tokenResponse.Content.ReadAsAsync<Token>(new[] { new JsonMediaTypeFormatter() }).Result;
//You can get the token with token.AccessToken object
}

Resources