https scheme issue with Swashbuckle using Proxied OWIN self-hosted Web API - c#

I have been working on a web api in C# using the self-hosted owin web api which is proxied.
I have setup the Swashbuckle.Core package, It all works fine. I am just having one slight issue with the base url.
When I go to my swagger ui docs page, it loads fine, But it tries to get https://api.domain.com:80/ instead of https://api.domain.com/ and the site says "Can't read from server. It may not have the appropriate access-control-origin settings.".
Here is my code that enables SwaggerUI:
configSwag.EnableSwagger("docs/{apiVersion}/swagger", c =>
{
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var commentsFileName = Assembly.GetExecutingAssembly().GetName().Name + ".XML";
var commentsFile = Path.Combine(baseDirectory, commentsFileName);
c.Schemes(new string[]
{
"https"
});
c.SingleApiVersion("v1", "MechaChat v1 Docs");
c.IncludeXmlComments(commentsFile);
c.PrettyPrint();
}).EnableSwaggerUi("v1/docs/{*assetPath}", c =>
{
c.DocExpansion(DocExpansion.List);
c.SupportedSubmitMethods("GET", "POST");
});
What would I need to do for the docs to get https://api.domain.com/ instead of https://api.domain.com:80/?

I ended up using NSwag, Seems to be working perfectly fine now!

Related

Why are .NET 6 Application + Application Gateway + Open ID Connect - Path based routing on different app services not working

I have an application gateway set up ("gateway"):
apps.mydomain.com
I have an app service set up ("app"):
my-app-service.azurewebsites.net
The path based rule is set on the listener for on the gateway address above.
/apps/app1/*
The default backend target and settings are set to the root of the gateway address above.
I am using AADS as the authentication store.
Both work correctly independently as I have another route set up on the gateway. I can go to the app service and it will prompt me for credentials, then take me to the index page at the root.
my-app-service.azurewebsites.net/
What I am trying to do is set up a path based rule that routes through the gateway and lands on a path under apps.mydomain.com. For example,
apps.mydomain.com/apps/app1.
I have set up the gateway properly as I can get to a static page. For example,
apps.mydomain.com/apps/app1/somedirectory/mystaticpage.html.
My problem is that when I try to authenticate, I think the signin-oidc is routing the request incorrectly. I am able to authenticate, and it appears to pass back to apps.mydomain.com/apps/app1/signin-oidc and then the middleware passes back to the root. It is authenticating, because when it hits the error page, it shows me as logged in.
I have tried overriding the cookie policy options:
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.Secure = CookieSecurePolicy.SameAsRequest;
options.MinimumSameSitePolicy = SameSiteMode.None;
//options.HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.None;
});
I have tried listening to the OnRedirectToIdentityProvider:
builder.Services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
//options.CallbackPath = new PathString("/apps/app1/");
//options.CallbackPath = new PathString("/apps/app1/signin-oidc");
//options.CallbackPath = "/apps/app1/signin-oidc";
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = (context) =>
{
//https://stackoverflow.com/questions/50262561/correlation-failed-in-net-core-asp-net-identity-openid-connect
context.Options.NonceCookie.Path = "https://apps.mydomain.com/apps/app1/signin-oidc";
context.Options.CorrelationCookie.Path = "https://apps.mydomain.com/apps/app1/signin-oidc";
//https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol#front-door-to-backend
context.ProtocolMessage.RedirectUri = "https://apps.mydomain.com/apps/app1/signin-oidc";
return Task.FromResult(0);
}
};
});
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy.
options.FallbackPolicy = options.DefaultPolicy;
});
My guess is that just setting the otions.CallbackPath should work, but I just get correlation or sorry, we cannot log you in errors when I try that. Not sure if there is an error in the library.
I have spent over a month on and off and engaged MS technical support trying to solve this, but have not been able to get this to work. I can't imagine I am the only one doing this. I know it is in the open ID connect middleware somewhere, but cannot find the correct combination.
This is just a demo project in .NET 6 to get this working correctly. Any code will do. If there is actual working code somewhere that would be great. Just need to get the path based routing with authentication to work.

APIM endpoint is not working through latest elastic client library

I am using latest "Elastic.Clients.Elasticsearch" library in .NET console application to talk to my elastic latest version 8.X. However, in my scenario I will not be talking directly to elastic I have a middle layer APIM endpoint. This is working fine when I am using NEST package with EnableAPIVersioningHeader setting. But in case of new library it throws 404 "resource not found error". Please can you let me know what are the changes that needs to be done to get this working.
Note: I am removing NEST package dependency from code, as Elastic will not support it in the future.
Sample Code:
public static ElasticsearchClient NewSearchClusterClient
{
get
{
var connectionSettings = new ElasticsearchClientSettings(new Uri("<apimendpoint>"));
connectionSettings.MaximumRetries(5);
connectionSettings.DefaultIndex("test");
connectionSettings.IncludeServerStackTraceOnError(true);
connectionSettings.EnableTcpStats(true);
connectionSettings.DisableDirectStreaming(true);
NameValueCollection collection = new NameValueCollection
{
};
connectionSettings.GlobalHeaders(collection);
var client = new ElasticsearchClient(connectionSettings);
return client;
}
}
Call this --> var respone = NewSearchClusterClient.Search(q => q.Query(m => m.MatchAll()));

502 Error: Bad Gateway on Azure App Service with IronPDF

I am attempting to get IronPDF working on my deployment of an ASP.NET Core 3.1 App Service.
I am not using Azure Functions for any of this, just a regular endpoints on an Azure App Service -which, when a user calls it, the service generates and returns a generated PDF document.
When running the endpoint on localhost, it works perfectly- generating the report from the HTML passed into the method. However, once I deploy it to my Azure Web App Service, I am getting a 502 - Bad Gateway error, as attached (displayed in Swagger for neatness sake).
Controller:
[HttpPost]
[Route("[action]")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> AgencyDownload([FromBody] AgencyReportSubmissionDto filters)
{
var user = await _userService.GetUserByIdAsync(HttpContext.User.GetUserId());
// Generate the PDF
var content = await _agencyReport.Generate(user, null, filters.FilterDate, filters.Content, filters.Type);
// Return the PDF to the browser
return new FileContentResult(content.BinaryData, "application/pdf") { FileDownloadName = "report.pdf" };
}
Service:
public async Task<PdfDocument> Generate(User user, byte[] letterhead, DateTimeOffset filterDate, string html, AgencyReportTypes reportType)
{
var corporateIdentity = new CorporateIdentity()
{
PrimaryColor = "#000000",
PrimaryTextColor = "#ffffff",
SecondaryColor = "#ffffff"
};
// Inserts the HTML content (from form) into the HTML template
var htmlContent = Template(corporateIdentity.PrimaryColor, corporateIdentity.PrimaryTextColor).Replace("{{HtmlContent}}", html);
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("South Africa Standard Time");
var convertedDate = TimeZoneInfo.ConvertTimeFromUtc(filterDate.UtcDateTime, tz);
var Renderer = new ChromePdfRenderer();
Renderer.RenderingOptions.Title = "Agency Report - for " + convertedDate.ToString("d MMMM yyyy");
Renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
var doc = await Renderer.RenderHtmlAsPdfAsync(htmlContent);
return doc;
}
Solution:
I noticed that if I performed a manual deployment to that app service, it was working, but when I was deploying from my pipeline- I had the error above.
So I went snooping around my pipelines and upon changing it to this, it worked.
- task: AzureRmWebAppDeployment#4
displayName: Deploy API Artifact
inputs:
ConnectionType: 'AzureRM'
AzureSubscription: 'My-Azure-Subscription'
enableCustomDeployment: true
DeploymentType: 'zipDeploy'
deployToSlotOrASE: true
SlotName: 'development'
AppType: 'webApp'
WebAppName: 'my-api'
Package: '$(Pipeline.Workspace)/**/API.zip'
ResourceGroupName: 'MyResource'
the 'DeploymentType: 'zipDeploy'" was key.
Thanks to Alex Hanneman for pointing me in the right direction.
I am also using Azure App Service as an API, wrapping IronPDF. Upgrading to latest Iron PDF package also broke my app, returning a 502.
What I did to fix it is deploy the code using ZipDeploy and then set WEBSITE_RUN_FROM_PACKAGE to 0. This is also needed to get the Iron PDF log files to show up in Azure, as they recommend here: https://iron.helpscoutdocs.com/article/122-azure-log-files.
App Service runs your apps in a sandbox and most PDF libraries will fail. Looking at the IronPDF documentation, they say that you can run it in a VM or a container. Since you already are using App Service, simply package your app in a container, publish it to a container registry and configure App Service to run it.
We are also facing the same issue with iron PDF but while changing the type of deployment to zip it's solves.

Asp.net core proxy keeps throwing 403 (asp.net core 3.1 + a bit of server-side blazor)

I'm using AspNetCore.Proxy to proxy the call to a static resource to an external service. I do some preprocessing to verify the user and to construct the URL to the external service (which I want to keep hidden from the user).
In my Startup.Configure() I have this:
app.UseProxies(builder =>
{
builder.Map("attached_resource/{id}", proxy =>
{
proxy.UseHttp((context, arguments) =>
{
var scope = context.RequestServices
.GetRequiredService<IServiceScopeFactory>().CreateScope();
using (scope)
{
var attachedResourceService = scope.ServiceProvider.GetService<AttachedResourceService>();
var providerContext = scope.ServiceProvider.GetService<ContentProviderContext>();
var resourceId = int.Parse(arguments["id"].ToString());
var sourceUrl = attachedResourceService.GetProviderResourceUrl(resourceId, providerContext.User.Id);
context.Request.Method = "POST";
context.Request.Headers.Add("auth", providerContext.User.Token);
return sourceUrl.Url;
}
});
});
});
The weird thing is; this works, but only when calling from Postman, directly from the address bar in the browser, or from a static HTML page. BUT... it returns a 403 every time I make the resource request from within my own app, from like a view or template.
So when I add an img tag to a static local plain HTML page it works:
<img src="https://localhost:5001/attached_resource/104" />
When I put the EXACT same img tag in a .cshtml view or layout, the chrome console (and the asp.net core app terminal) shows returning 403.
Using asp.net core 3.1 with a few server-side rendered Blazor components. So a plain MVC app, not an all-out Blazor app.
I must be missing something very obvious. Any ideas? Thanks for thinking along with me.

Google Data API Authorization Redirect URI Mismatch

Background
I am wanting to write a small, personal web app in .NET Core 1.1 to interact with YouTube and make some things easier for me to do and I am following the tutorials/samples in Google's YouTube documentation. Sounds simple enough, right? ;)
Authenticating with Google's APIs seems impossible! I have done the following:
Created an account in the Google Developer Console
Created a new project in the Google Developer Console
Created a Web Application OAuth Client ID and added my Web App debug URI to the list of approved redirect URIs
Saved the json file provided after generating the OAuth Client ID to my system
In my application, my debug server url is set (and when my application launches in debug, it's using the url I set which is http://127.0.0.1:60077).
However, when I attempt to authenticate with Google's APIs, I recieve the following error:
That’s an error.
Error: redirect_uri_mismatch
The redirect URI in the request, http://127.0.0.1:63354/authorize/,
does not match the ones authorized for the OAuth client.
Problem
So now, for the problem. The only thing I can find when searching for a solution for this is people that say
just put the redirect URI in your approved redirect URIs
Unfortunately, the issue is that every single time my code attempts to authenticate with Google's APIs, the redirect URI it is using changes (the port changes even though I set a static port in the project's properties). I cannot seem to find a way to get it to use a static port. Any help or information would be awesome!
NOTE: Please don't say things like "why don't you just do it this other way that doesn't answer your question at all".
The code
client_id.json
{
"web": {
"client_id": "[MY_CLIENT_ID]",
"project_id": "[MY_PROJECT_ID]",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": "[MY_CLIENT_SECRET]",
"redirect_uris": [
"http://127.0.0.1:60077/authorize/"
]
}
}
Method That Is Attempting to Use API
public async Task<IActionResult> Test()
{
string ClientIdPath = #"C:\Path\To\My\client_id.json";
UserCredential credential;
using (var stream = new FileStream(ClientIdPath, FileMode.Open, FileAccess.Read))
{
credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
new[] { YouTubeService.Scope.YoutubeReadonly },
"user",
CancellationToken.None,
new FileDataStore(this.GetType().ToString())
);
}
var youtubeService = new YouTubeService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = this.GetType().ToString()
});
var channelsListRequest = youtubeService.Channels.List("contentDetails");
channelsListRequest.Mine = true;
// Retrieve the contentDetails part of the channel resource for the authenticated user's channel.
var channelsListResponse = await channelsListRequest.ExecuteAsync();
return Ok(channelsListResponse);
}
Project Properties
The Original Answer works, but it is NOT the best way to do this for an ASP.NET Web Application. See the update below for a better way to handle the flow for an ASP.NET Web Application.
Original Answer
So, I figured this out. The issue is that Google thinks of a web app as a JavaScript based web application and NOT a web app with server side processing. Thus, you CANNOT create a Web Application OAuth Client ID in the Google Developer Console for a server based web application.
The solution is to select the type Other when creating an OAuth Client ID in the Google Developer Console. This will have Google treat it as an installed application and NOT a JavaScript application, thus not requiring a redirect URI to handle the callback.
It's somewhat confusing as Google's documentation for .NET tells you to create a Web App OAuth Client ID.
Feb 16, 2018 Updated Better Answer:
I wanted to provide an update to this answer. Though, what I said above works, this is NOT the best way to implement the OAuth workflow for a ASP.NET solution. There is a better way which actually uses a proper OAuth 2.0 flow. Google's documentation is terrible in regards to this (especially for .NET), so I'll provide a simple implementation example here. The sample is using ASP.NET core, but it's easily adapted to the full .NET framework :)
Note: Google does have a Google.Apis.Auth.MVC package to help simplifiy this OAuth 2.0 flow, but unfortunately it's coupled to a specific MVC implementation and does not work for ASP.NET Core or Web API. So, I wouldn't use it. The example I'll be giving will work for ALL ASP.NET applications. This same code flow can be used for any of the Google APIs you've enabled as it's dependent on the scopes you are requesting.
Also, I am assuming you have your application set up in your Google Developer dashboard. That is to say that you have created an application, enabled the necessary YouTube APIs, created a Web Application Client, and set your allowed redirect urls properly.
The flow will work like this:
The user clicks a button (e.g. Add YouTube)
The View calls a method on the Controller to obtain an Authorization URL
On the controller method, we ask Google to give us an Authorization URL based on our client credentials (the ones created in the Google Developer Dashboard) and provide Google with a Redirect URL for our application (this Redirect URL must be in your list of accepted Redirect URLs for your Google Application)
Google gives us back an Authorization URL
We redirect the user to that Authorization URL
User grants our application access
Google gives our application back a special access code using the Redirect URL we provided Google on the request
We use that access code to get the Oauth tokens for the user
We save the Oauth tokens for the user
You need the following NuGet Packages
Google.Apis
Google.Apis.Auth
Google.Apis.Core
Google.apis.YouTube.v3
The Model
public class ExampleModel
{
public bool UserHasYoutubeToken { get; set; }
}
The Controller
public class ExampleController : Controller
{
// I'm assuming you have some sort of service that can read users from and update users to your database
private IUserService userService;
public ExampleController(IUserService userService)
{
this.userService = userService;
}
public async Task<IActionResult> Index()
{
var userId = // Get your user's ID however you get it
// I'm assuming you have some way of knowing if a user has an access token for YouTube or not
var userHasToken = this.userService.UserHasYoutubeToken(userId);
var model = new ExampleModel { UserHasYoutubeToken = userHasToken }
return View(model);
}
// This is a method we'll use to obtain the authorization code flow
private AuthorizationCodeFlow GetGoogleAuthorizationCodeFlow(params string[] scopes)
{
var clientIdPath = #"C:\Path\To\My\client_id.json";
using (var fileStream = new FileStream(clientIdPath, FileMode.Open, FileAccess.Read))
{
var clientSecrets = GoogleClientSecrets.Load(stream).Secrets;
var initializer = new GoogleAuthorizationCodeFlow.Initializer { ClientSecrets = clientSecrets, Scopes = scopes };
var googleAuthorizationCodeFlow = new GoogleAuthorizationCodeFlow(initializer);
return googleAuthorizationCodeFlow;
}
}
// This is a route that your View will call (we'll call it using JQuery)
[HttpPost]
public async Task<string> GetAuthorizationUrl()
{
// First, we need to build a redirect url that Google will use to redirect back to the application after the user grants access
var protocol = Request.IsHttps ? "https" : "http";
var redirectUrl = $"{protocol}://{Request.Host}/{Url.Action(nameof(this.GetYoutubeAuthenticationToken)).TrimStart('/')}";
// Next, let's define the scopes we'll be accessing. We are requesting YouTubeForceSsl so we can manage a user's YouTube account.
var scopes = new[] { YouTubeService.Scope.YoutubeForceSsl };
// Now, let's grab the AuthorizationCodeFlow that will generate a unique authorization URL to redirect our user to
var googleAuthorizationCodeFlow = this.GetGoogleAuthorizationCodeFlow(scopes);
var codeRequestUrl = googleAuthorizationCodeFlow.CreateAuthorizationCodeRequest(redirectUrl);
codeRequestUrl.ResponseType = "code";
// Build the url
var authorizationUrl = codeRequestUrl.Build();
// Give it back to our caller for the redirect
return authorizationUrl;
}
public async Task<IActionResult> GetYoutubeAuthenticationToken([FromQuery] string code)
{
if(string.IsNullOrEmpty(code))
{
/*
This means the user canceled and did not grant us access. In this case, there will be a query parameter
on the request URL called 'error' that will have the error message. You can handle this case however.
Here, we'll just not do anything, but you should write code to handle this case however your application
needs to.
*/
}
// The userId is the ID of the user as it relates to YOUR application (NOT their Youtube Id).
// This is the User ID that you assigned them whenever they signed up or however you uniquely identify people using your application
var userId = // Get your user's ID however you do (whether it's on a claim or you have it stored in session or somewhere else)
// We need to build the same redirect url again. Google uses this for validaiton I think...? Not sure what it's used for
// at this stage, I just know we need it :)
var protocol = Request.IsHttps ? "https" : "http";
var redirectUrl = $"{protocol}://{Request.Host}/{Url.Action(nameof(this.GetYoutubeAuthenticationToken)).TrimStart('/')}";
// Now, let's ask Youtube for our OAuth token that will let us do awesome things for the user
var scopes = new[] { YouTubeService.Scope.YoutubeForceSsl };
var googleAuthorizationCodeFlow = this.GetYoutubeAuthorizationCodeFlow(scopes);
var token = await googleAuthorizationCodeFlow.ExchangeCodeForTokenAsync(userId, code, redirectUrl, CancellationToken.None);
// Now, you need to store this token in rlation to your user. So, however you save your user data, just make sure you
// save the token for your user. This is the token you'll use to build up the UserCredentials needed to act on behalf
// of the user.
var tokenJson = JsonConvert.SerializeObject(token);
await this.userService.SaveUserToken(userId, tokenJson);
// Now that we've got access to the user's YouTube account, let's get back
// to our application :)
return RedirectToAction(nameof(this.Index));
}
}
The View
#using YourApplication.Controllers
#model YourApplication.Models.ExampleModel
<div>
#if(Model.UserHasYoutubeToken)
{
<p>YAY! We have access to your YouTube account!</p>
}
else
{
<button id="addYoutube">Add YouTube</button>
}
</div>
<script>
$(document).ready(function () {
var addYoutubeUrl = '#Url.Action(nameof(ExampleController.GetAuthorizationUrl))';
// When the user clicks the 'Add YouTube' button, we'll call the server
// to get the Authorization URL Google built for us, then redirect the
// user to it.
$('#addYoutube').click(function () {
$.post(addYoutubeUrl, function (result) {
if (result) {
window.location.href = result;
}
});
});
});
</script>
As referred here, you need to specify a fix port for the ASP.NET development server like How to fix a port number in asp.NET development server and add this url with the fix port to the allowed urls. Also as stated in this thread, when your browser redirects the user to Google's oAuth page, you should be passing as a parameter the redirect URI you want Google's server to return to with the token response.
I noticed that there is easy non-programmatic way around.
If you have typical monotlith application built in typical MS convention(so not compatible with 12factor and typical DDD) there is an option to tell your Proxy WWW server to rewrite all requests from HTTP to HTTPS so even if you have set up Web App on http://localhost:5000 and then added in Google API url like: http://your.domain.net/sigin-google, it will work perfectly and it is not that bas because it is much safer to set up main WWW to rewrite all to HTTPS.
It is not very good practice I guess however it makes sense and does the job.
I've struggled with this issue for hours in a .net Core application. What finally fixed it for me was, in the Google developers console, to create and use a credential for "Desktop app" instead of a "Web application".
Yeah!! Using credentials of desktop app instead of web app worked for me fine. It took me more than 2 days to figure out this problem. The main problem is that google auth library dose not adding or supporting http://localhost:8000 as redirect uri for web app creds but credentials of desktop app fixed that issue. Cause its supporting http://___ connection instead of https: connection for redirect uri

Categories