Serving files with authorization in an Owin self hosted application - c#

I have a self hosted application using Owin and no ASP.MVC of any type, so there is no web.config in the application.
I have enabled cookie authentication and my own authorization provider mechanism which works perfectly fine.
My application serves some static contents using the the next code:
appBuilder.UseFileServer(new FileServerOptions()
{
RequestPath = new PathString("/Images"),
FileSystem = new PhysicalFileSystem(#"./Images"),
});
But that content is not protected by the Owin authentication, what would be the easiest way to protect the files?
*ideally not having to implement the whole file serving myself.

So far I've managed to do it in this way:
var contentFileServer = new FileServerOptions()
{
RequestPath = new PathString("/Content"),
FileSystem = new PhysicalFileSystem(#"./Content"),
};
contentFileServer.StaticFileOptions.OnPrepareResponse = (context) =>
{
if (context.OwinContext.Authentication.User == null)
{
// Reply an unauthorized
context.OwinContext.Response.StatusCode = 401;
}
};
appBuilder.UseFileServer(contentFileServer);
Looks like a reasonable way of doing it.

#Cristian T answer needs to be extended. See my comment: "This only sets the HTTP status code, under the hood it also sends the file requested, you can see it in the Network tab in browsers console..."
So, to disable sending the file you have to write the response yourself, before the StaticFileContext will. The OnPrepareResponse function should be:
contentFileServer.StaticFileOptions.OnPrepareResponse = (context) =>
{
if (context.OwinContext.Authentication.User == null)
{
// Reply an unauthorized
const string unauthorizedBody = "Unauthorized"; // or HTML or anything else
ctx.OwinContext.Response.StatusCode = 401;
ctx.OwinContext.Response.Headers.Set("Content-Length", unauthorizedBody.Length.ToString());
ctx.OwinContext.Response.Headers.Set("Content-Type", "text/html");
ctx.OwinContext.Response.Write(unauthorizedBody);
}
};
This way, the server returns "Unauthorized" instead of the file requested.

Related

ASP.NET MVC website application to upload files with Google Drive API URI mismatch

I'm trying to upload files using the Google Drive API and am getting a URI mismatch error from Google when clicking the upload button on my page. The URI that Google shows isn't even a part of the website, nor is a URI that I supplied to Google, so I have no idea where it's coming from.
Here is the APIHelper class I created based off of this tutorial (which shows that the code should work on a website)
public class GoogleDriveAPIHelper
{
//add scope
public static string[] Scopes = { DriveService.Scope.Drive };
//create Drive API service.
public static DriveService GetService()
{
//get Credentials from client_secret.json file
UserCredential credential;
//Root Folder of project
var CSPath = System.Web.Hosting.HostingEnvironment.MapPath("~/");
using (var stream = new FileStream(Path.Combine(CSPath, "client_secret.json"), FileMode.Open, FileAccess.Read))
{
string FolderPath = System.Web.Hosting.HostingEnvironment.MapPath("~/");
string FilePath = Path.Combine(FolderPath, "DriveServiceCredentials.json");
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
Scopes,
"user",
CancellationToken.None,
new FileDataStore(FilePath, true)).Result;
}
//create Drive API service.
DriveService service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Documents Uploader",
});
return service;
}
//file Upload to the Google Drive.
public static void UploadFile(string folderID, HttpPostedFileBase file)
{
if (file != null && file.ContentLength > 0)
{
//create service
DriveService service = GetService();
string path = Path.Combine(HttpContext.Current.Server.MapPath("~/GoogleDriveFiles"),
Path.GetFileName(file.FileName));
file.SaveAs(path);
var FileMetaData = new Google.Apis.Drive.v3.Data.File
{
Name = Path.GetFileName(file.FileName),
MimeType = MimeMapping.GetMimeMapping(path),
//id of parent folder
Parents = new List<string>
{
folderID
}
};
FilesResource.CreateMediaUpload request;
using (var stream = new FileStream(path, FileMode.Open))
{
request = service.Files.Create(FileMetaData, stream, FileMetaData.MimeType);
request.Fields = "id";
request.Upload();
}
}
}
}
And the post
[HttpPost]
public ActionResult Index(HttpPostedFileBase file)
{
string folderID = "1L9QUUgmtg8KUdNvutQ1yncIwN_uLz4xs";
if (TempData["Success"] == null)
{
// show all fields
ViewBag.ShowForm = true;
ViewBag.ShowButtons = false;
}
else
{
// hide all elements on the page for success message
ViewBag.ShowForm = false;
ViewBag.ShowButtons = true;
}
GoogleDriveAPIHelper.UploadFile(folderID, file);
TempData["Success"] = "File successfully uploaded";
return View();
}
I have heard that the tutorial is referencing code that only works for standalone apps and not web apps, so it's odd that the screenshots in the tutorial are from a website. shrug I'll keep looking for tips and tricks, but in the meantime, I'm posting this to see if anyone else has written a site to upload through the Google drive to a specific folder, not the root. TIA!
Edit: Here are screenshots of the redirect URI I set up in the Google Cloud Console. Prod & localhost
Edit: Startup.Auth.cs - this is used for pass through ADFS authentication and has nothing to do with the Google Drive API
private void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
// TempData and Owin don't get along, use this workaround to force a custom cookie manager
// https://stackoverflow.com/questions/28559237/intermittent-redirection-loops-during-adfs-authentication
CookieManager = new SystemWebCookieManager()
});
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = ConfigurationManager.AppSettings["ida:Wtrealm"],
MetadataAddress = ConfigurationManager.AppSettings["ida:ADFSMetadata"]
});
}
The realm matches the URI in the Google console and the metadata is the same xml link I use in all my web apps that use ADFS pass through auth, which has worked flawlessly. Nothing in my web.config file mention the IP address that Google says is my redirect URI either.
The URI that Google shows isn't even a part of the website, nor is a URI that I supplied to Google, so I have no idea where it's coming from.
The redirect uri is built buy the client library you are using. Your app is set to run http not https its running localhost and not hosted so its 127.0.0.1 the port is also either being randomly generated by your app or something that you have set up statically. the /authorize is attached again by the client library.
The redirect uri is the location your code is prepared to accept the response from the authorization server. This URI needs to be configured in Google cloud console. The easiest solution is to copy it exactly and add it as a redirect uri in Google cloud console. Just make sure that your app is set to use a static port if the port changes its not going to work.
This video will show you how to add it. Google OAuth2: How the fix redirect_uri_mismatch error. Part 2 server sided web applications.
Web applications
public void ConfigureServices(IServiceCollection services)
{
...
// This configures Google.Apis.Auth.AspNetCore3 for use in this app.
services
.AddAuthentication(o =>
{
// This forces challenge results to be handled by Google OpenID Handler, so there's no
// need to add an AccountController that emits challenges for Login.
o.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// This forces forbid results to be handled by Google OpenID Handler, which checks if
// extra scopes are required and does automatic incremental auth.
o.DefaultForbidScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// Default scheme that will handle everything else.
// Once a user is authenticated, the OAuth2 token info is stored in cookies.
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogleOpenIdConnect(options =>
{
options.ClientId = {YOUR_CLIENT_ID};
options.ClientSecret = {YOUR_CLIENT_SECRET};
});
}

Windows Authentication with IdentityServer4 on IIS

I am trying to use Windows Authentication credentials to connect with my native (Winforms, console app) client to Identity Server hosted on IIS. The point is for user to be authenticated by AD and with those credentials get the right claims and roles from the Identity Server (which is run through commercial https://commercial.abp.io/ platform).
EDIT:
I found out it is not client related issue since i cannot use my External login (Windows credentials) even directly on hosted site.
The thing worked locally while hosted by IISExpress, then i published it to IIS and enabled the Anonymous and Windows Authentication in the IIS settings and here is where problems began.
When i run it and click the External Login (Windows Credentials) button i usually get a redirect to https://myserver/Error?httpStatusCode=401
and i get prompt for my windows credentials (which even if i insert correctly, just repeat prompt again).
From time to time i get logged in with my Windows credentials (which is the goal). Login with username and password works fine.
I saw the similar issue mentioned by someone here:
https://github.com/IdentityServer/IdentityServer4/issues/4937 without any solution\answer.
My client is basically the sample NativeConsolePKCEClient from this https://github.com/damienbod/AspNetCoreWindowsAuth
static string _authority = "https://myserver/";
string redirectUri = "https://127.0.0.1:45656";
var options = new OidcClientOptions
{
Authority = _authority,
ClientId = "native.code",
ClientSecret = "secret",
RedirectUri = redirectUri,
Scope = "openid profile",
FilterClaims = false,
Browser = browser,
Flow = OidcClientOptions.AuthenticationFlow.AuthorizationCode,
ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect,
LoadProfile = true
};
_oidcClient = new OidcClient(options);
var result = await _oidcClient.LoginAsync();
and on server side the startup configuration services:
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.Configure<IISOptions>(iis => // IISOptions
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
context.Services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); ;
options.Audience = "ABPIdentityServer";
});
}
Here is the ProcessWindowsLoginAsync challenge method:
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync(Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme);
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, tresting windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = "./ExternalLoginCallback",
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme },
}
};
var id = new ClaimsIdentity(Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
// add the groups as claims -- be careful if the number of groups is too large
{
var wi = (WindowsIdentity)wp.Identity;
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props);
return Redirect(props.RedirectUri);
}
else
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge("Windows");
}
}
I am suspecting that this piece of code when calling Challenge somehow returns up redirecting to error page, but i am not sure and i do now why.
So what am i missing? Is it even possible to run both Windows and Anonymous authentication on IIS?
Here i also found similar issue:
identity server 4 windows authentication
but the presented answers did not help me.
I strongly suspect that it's not the client issue it's the token provider's issue (Not the ID4 library but one where you have installed the ID4 library).
I believe that you have added the below code in the AccountController->Login action but make sure that you have added a success check in it, if you miss that then your app will go infinite loop.
[HttpGet] public async Task<IActionResult> Login(string returnUrl)
{
if(loginViewModel.ExternalLoginScheme == "Windows")
{
var authenticationResult = await HttpContext.AuthenticateAsync("Windows").ConfigureAwait(false);
if (authenticationResult.Succeeded && authenticationResult?.Principal is WindowsPrincipal windowsPrinciple)
{
// Add your custom code here
var authProps = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", "Windows"},
}
};
await HttpContext.SignInAsync();
return Redirect(RedirectUri);
}
else
{
return Challenge("Windows");
}
}
}
I hope this will help you to fix your issue.
Happy Coding!!
Just for anybody who might be interested. I found out what was causing the redirect error.
It is somehow connected with the ABP Suite i used for generating the base application.
there in the ApplicationInitialization there was a middleware called
app.UseErrorPage();
Which when the Windows credentials were challenged took it as an Error and redirected to https://myserver/Error?httpStatusCode=401.
I am not sure how this middleware works and why sometimes login worked, but removing this part solved my issue.
I hope this helps somebody, somehow, sometime..

Azure Bot project with client and server in same web app

I've made an Azure bot application using the BotFramework v4 and used the WebChat control as an interface. I noticed that the bot server's dotnetcore app had a wwwroot folder with a placeholder HTML page in it, so thought it might be expedient to host the webchat client there. But now seems counter-intuitive that my webchat client is using DirectLine to send activities back to the same back-end that served it.
I had chosen the webchat client because I need to customise the appearance of the client. I also need the MVC app that serves the bot client to include Azure Active Directory B2C authentication (which it does). Users should see the webchat client before and after authentication but the bot back-end (handling the activities) needs to know whether the user is logged in and modify its behaviour accordingly (and I am struggling to achieve that part with DirectLine).
So my first question (ever on StackOverflow) is: With the Bot back-end and the webchat client front-end being hosted in the same, single Azure web app, is it necessary to use DirectLine, or is there a simpler way of doing this?
Relevant code in my Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
app.UseStaticFiles(); // to allow serving up the JS, CSS, etc., files.
app.UseBotFramework(); // to add middleware to route webchat activity to the bot back-end code
app.UseSession(); // to enable session state
app.UseAuthentication(); // to enable authentication (in this case AAD B2C)
app.UseMvcWithDefaultRoute(); // to add MVC middleware with default route
}
Also in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// standard code to add HttpContextAssessor, BotServices, BotConfigs and memory storage singletons ommitted for brevity ...
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAdB2C(options => Configuration.Bind("Authentication:AzureAdB2C", options))
.AddCookie();
services.AddMvc();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(1);
options.CookieHttpOnly = true;
});
// Create and add conversation state.
var conversationState = new ConversationState(dataStore);
services.AddSingleton(conversationState);
var userState = new UserState(dataStore);
services.AddSingleton(userState);
services.AddBot<MyBot>(options =>
{
options.CredentialProvider = new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);
options.ChannelProvider = new ConfigurationChannelProvider(Configuration);
// Catches any errors that occur during a conversation turn and logs them to currently
// configured ILogger.
ILogger logger = _loggerFactory.CreateLogger<RucheBot>();
options.OnTurnError = async (context, exception) =>
{
logger.LogError($"Exception caught : {exception}");
await context.SendActivityAsync("Sorry, it looks like something went wrong.");
};
});
}
My controller's Index method:
public async Task<ActionResult> Index()
{
string userId;
if (User.Identity.IsAuthenticated)
{
string aadb2cUserId = User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier").Value;
Users.EnsureAccountExists(aadb2cUserId); // ensure account with given AAD identifier is know locally (by creating it if not)
userId = $"ia_{aadb2cUserId}";
}
else
{
userId = $"na_{Guid.NewGuid()}";
}
HttpClient client = new HttpClient();
string directLineUrl = $"https://directline.botframework.com/v3/directline/tokens/generate";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, directLineUrl);
// TODO: put this in the config somewhere
var secret = "<the secret code from my bot's DirectLine channel config in the Azure portal>";
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secret);
string jsonUser = JsonConvert.SerializeObject(new { User = new { Id = userId } });
request.Content = new StringContent(jsonUser, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
string token = string.Empty;
if (response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync();
token = JsonConvert.DeserializeObject<DirectLineToken>(body).token;
}
var config = new ChatConfig()
{
Token = token,
UserId = userId,
};
return View(config);
}
And finally the code in the associated view:
<div id="webchat"></div>
<script type="text/javascript">
...
/// Called asynchronously during the page load
function renderWebChat( withSound )
{
var webchatOptions =
{
directLine: window.WebChat.createDirectLine( { secret: '#Model.Token'} ),
userID: '#Model.UserId'
};
if ( withSound )
{
webchatOptions.webSpeechPonyfillFactory = window.WebChat.createBrowserWebSpeechPonyfillFactory();
}
window.WebChat.renderWebChat( webchatOptions, document.getElementById( 'webchat' ) );
document.querySelector( '#webchat > *' ).focus();
}
</script>
I'm going to disagree with Nicolas R a little bit. When it comes to directly accessing your bot, you might like to have a look at this: https://www.npmjs.com/package/offline-directline
There's also the option of hosting a bot in the browser, which I think may facilitate the sort of direct communication you're looking for.
Long question, but the answer will be a lot shorter!
So my first question (ever on StackOverflow) is: With the Bot back-end
and the webchat client front-end being hosted in the same, single
Azure web app, is it necessary to use DirectLine, or is there a
simpler way of doing this?
Yes, it is necessary. In fact, all the channels types are using the Bot Connector to communicate with your backend (your bot code), there is no direct access possible. There are a lot of reasons for that, one is for example the billing!

For refresh flow, do I need to create an OpenIddictApplicationDescriptor for my client?

I'm using the refresh flow sample, but i added the instrospection middleware since my resource server is separated from my authorization server. Using the angular app, I'm able to connect to the authorization server. Everything works. But I noticed in the Implicit Flow sample that the following code was added:
if (await manager.FindByClientIdAsync("aurelia", cancellationToken) == null)
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "aurelia",
DisplayName = "Aurelia client application",
PostLogoutRedirectUris = { new Uri("http://localhost:9000/signout-oidc") },
RedirectUris = { new Uri("http://localhost:9000/signin-oidc") }
};
await manager.CreateAsync(descriptor, cancellationToken);
}
Do I need to add something similar to this for the Refresh flow?
If you don't configure OpenIddict to require a client_id for token requests (by calling options.RequireClientIdentification()) then creating an application in the database is not mandatory to be able to use the refresh token flow.

IdentityServer3 - rejected because invalid CORS path

We have an ASP.NET MVC application that is authenticating without issue against IdentityServer3, however the web API part of the application using ApiController's start to fail if the user waits before proceeding with AJAX functionality after about 3 minutes (before 3 mins everything seems fine).
The errors seen in Chrome are:
XMLHttpRequest cannot load
https://test-auth.myauthapp.com/auth/connect/authorize?client_id=ecan-farmda…gwLTk5ZjMtN2QxZjUyMjgxNGE4MDg2NjFhZTAtOTEzNi00MDE3LTkzNGQtNTc5ODAzZTE1Mzgw.
No 'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://test.myapp.com' is therefore not allowed
access.
On IE I get the following errors:
SCRIPT7002: XMLHttpRequest: Network Error 0x4c7, The operation was
canceled by the user.
Looking at IdentityServer3's logs I'm seeing entries like so:
2015-08-10 16:42 [Warning]
(Thinktecture.IdentityServer.Core.Configuration.Hosting.CorsPolicyProvider)
CORS request made for path: /connect/authorize from origin:
http://test.myapp.com but rejected because invalid CORS path
In the IdentityServer3 web application I'm giving clients AllowedCorsOrigins:
Thinktecture.IdentityServer.Core.Models.Client client = new Thinktecture.IdentityServer.Core.Models.Client()
{
Enabled = configClient.Enabled,
ClientId = configClient.Id,
ClientName = configClient.Name,
RedirectUris = new List<string>(),
PostLogoutRedirectUris = new List<string>(),
AllowedCorsOrigins = new List<string>(),
RequireConsent = false, // Don't show consents screen to user
RefreshTokenExpiration = Thinktecture.IdentityServer.Core.Models.TokenExpiration.Sliding
};
foreach (Configuration.RegisteredUri uri in configClient.RedirectUris)
{
client.RedirectUris.Add(uri.Uri);
}
foreach (Configuration.RegisteredUri uri in configClient.PostLogoutRedirectUris)
{
client.PostLogoutRedirectUris.Add(uri.Uri);
}
// Quick hack to try and get CORS working
client.AllowedCorsOrigins.Add("http://test.myapp.com");
client.AllowedCorsOrigins.Add("http://test.myapp.com/"); // Don't think trailing / needed, but added just in case
clients.Add(client);
And when registering the service I add a InMemoryCorsPolicyService:
app.Map("/auth", idsrvApp =>
{
var factory = new IdentityServerServiceFactory();
factory.Register(new Registration<AuthContext>(resolver => AuthObjects.AuthContext));
factory.Register(new Registration<AuthUserStore>());
factory.Register(new Registration<AuthRoleStore>());
factory.Register(new Registration<AuthUserManager>());
factory.Register(new Registration<AuthRoleManager>());
// Custom user service used to inject custom registration workflow
factory.UserService = new Registration<IUserService>(resolver => AuthObjects.AuthUserService);
var scopeStore = new InMemoryScopeStore(Scopes.Get());
factory.ScopeStore = new Registration<IScopeStore>(scopeStore);
var clientStore = new InMemoryClientStore(Clients.Get());
factory.ClientStore = new Registration<IClientStore>(clientStore);
var cors = new InMemoryCorsPolicyService(Clients.Get());
factory.CorsPolicyService = new Registration<ICorsPolicyService>(cors);
...
var options = new IdentityServerOptions
{
SiteName = "Authentication",
SigningCertificate = LoadCertificate(),
Factory = factory,
AuthenticationOptions = authOptions
};
...
});
I do note that the IdentityServer3 log entries say "CORS request made for path: /connect/authorize" rather than "CORS request made for path: /auth/connect/authorize". But looking through the IdentityServer3 source code suggests this probably isn't the issue.
Perhaps the InMemoryCorsPolicyService isn't being picked up?
Any ideas of why things aren't working for the AJAX called ApiController?
Thinktecture.IdevtityServer3 v1.6.2 has been installed using NuGet.
Update
I'm having a conversation with the IdentityServer3 developer, but am still having an issue reaching a resolution. In case it helps:
https://github.com/IdentityServer/IdentityServer3/issues/1697
Did you try adding https url also?- client.AllowedCorsOrigins.Add("https://test.myapp.com");
The documentation of IdentityServer says you should configure it on the client:
AllowedCorsOrigins = ... // Defaults to the discovery, user info, token, and revocation endpoints.
https://docs.duendesoftware.com/identityserver/v6/reference/options/#cors
CORS is a nightmare!
It's a browser thing which is why you're witnessing different behaviour in IE than in Chrome.
There are (at least) two ways that CORS is configured on the server. When a client makes a request with the Origin header you have to tell the server whether or not to accept it -- if accepted then the server adds the Access-Control-Allow-Origin header to the response for the browser.
In MVC / webAPI you have to add CORS services, set a CORS policy, and then .UseCors something like this:
builder.Services.AddCors((options =>
{
if (settings.AllowedCorsOrigins.Length > 0)
{
options.AddDefaultPolicy(builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().WithOrigins(settings.AllowedCorsOrigins);
});
}
if (isDevelopment)
{
options.AddPolicy("localhost", builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().SetIsOriginAllowed((string origin) => { return origin.Contains("localhost"); }); });
}
});
and
app.UseCors();
if (app.Environment.IsDevelopment())
{
app.UseCors("localhost");
}
Typically, you want the list of allowed hosts as an array of strings in your appsettings.json. And watch out for the boobytrap with SetIsOriginAllowedToAllowWildcardSubdomains.
As well as this, IdentityServer has its own additional CORS settings which are applied in addition to the standard MVC/webAPI settings. These are in the ClientCorsOrigin table and this doesn't support wildcard subdomains. You can sidestep this whole boobytrap by implementing your own ICorsPolicyService to use the same settings from your appsettings.json something like this
public class CorsPolicyService : ICorsPolicyService
{
private readonly CorsOptions _options;
public CorsPolicyService(IOptions<CorsOptions> options)
{
_options = options.Value;
}
private bool CheckHost(string host)
{
foreach (string p in _options.AllowedCorsOrigins)
{
if (Regex.IsMatch(host, Regex.Escape(p).Replace("\\*", "[a-zA-Z0-9]+"))) // Hyphen?
{
return true;
}
}
return false;
}
public Task<bool> IsOriginAllowedAsync(string origin)
{
return Task.FromResult(CheckHost(origin));
}
}

Categories