I'm working on a Blazor project (asp.net core hosted) that is published on a Linux host, with Nginx which is working as a web server and proxy to redirect calls to my domain with a specific port to my application. (https://example:9999 => localhost:10000 for example)
I integrated OpenIddict (OAuth) for authorization since IdentityServer needs a custom license on the latest .net version.
The problem I currently face only happens on production, when OpenIddict redirects to the login page with the custom route parameters, the URL is missing the custom port I'm using on production. On localhost (Dev / local machine) the correct port is supplied https://localhost:7115, but on production, I'm redirected to https://example.com/Identity/Account/Login, instead of https://example.com:9999/Identity/Account/Login.
When I'm changing the URL manually everything works fine and I can correctly login to my application.
The OpenIddict Server settings:
builder.Services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIddictConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIddictConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role;
});
builder.Services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
builder.Services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
options.UseQuartz();
})
.AddServer(options =>
{
options.SetIssuer(new Uri(publicHostFullUrl));
options.SetAuthorizationEndpointUris(authorizationEndpoint)
.SetLogoutEndpointUris(logoutEndpoint)
.SetTokenEndpointUris(tokenEndpoint)
.SetUserinfoEndpointUris(userInfoEndpoint);
options.RegisterScopes(OpenIddictConstants.Scopes.Email, OpenIddictConstants.Permissions.Scopes.Profile, OpenIddictConstants.Permissions.Scopes.Roles);
options.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow();
options.AddEncryptionCertificate(certificate)
.AddSigningCertificate(certificate);
}
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableStatusCodePagesIntegration()
.EnableTokenEndpointPassthrough();
options.AcceptAnonymousClients();
options.DisableScopeValidation();
})
.AddValidation(options =>
{
options.SetIssuer(new Uri(publicHostFullUrl));
options.UseLocalServer();
options.UseAspNetCore();
});
The Client settings:
builder.Services.AddOidcAuthentication(options =>
{
options.ProviderOptions.ClientId = clientId;
options.ProviderOptions.Authority = $"{builder.HostEnvironment.BaseAddress}";
options.ProviderOptions.ResponseType = "code";
options.ProviderOptions.ResponseMode = "query";
options.AuthenticationPaths.RemoteRegisterPath = $"{builder.HostEnvironment.BaseAddress}Identity/Account/Register";
options.AuthenticationPaths.LogInCallbackPath = $"{builder.HostEnvironment.BaseAddress}/Identity/Account/Login";
options.AuthenticationPaths.LogInPath = $"{builder.HostEnvironment.BaseAddress}/Identity/Account/Login";
});
My Authorization Controller action where I return the ChallengeResult to the Login Page:
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
if (request.HasPrompt(Prompts.Login))
{
var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
var parameters = Request.HasFormContentType ? Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() : Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();
parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
});
}
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (!result.Succeeded || (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)))
{
if (request.HasPrompt(Prompts.None))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}!));
}
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
var user = await _userManager.GetUserAsync(result.Principal) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
var authorizations = await _authorizationManager.FindAsync(
subject: await _userManager.GetUserIdAsync(user),
client: (await _applicationManager.GetIdAsync(application))!,
status: Statuses.Valid,
type: AuthorizationTypes.Permanent,
scopes: request.GetScopes()).ToListAsync();
switch (await _applicationManager.GetConsentTypeAsync(application))
{
case ConsentTypes.External when !authorizations.Any():
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}!));
case ConsentTypes.Implicit:
case ConsentTypes.External when authorizations.Any():
case ConsentTypes.Explicit when authorizations.Any() && !request.HasPrompt(Prompts.Consent):
var principal = await _signInManager.CreateUserPrincipalAsync(user);
principal.SetScopes(request.GetScopes());
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
var authorization = authorizations.LastOrDefault();
if (authorization == null)
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
subject: await _userManager.GetUserIdAsync(user),
client: (await _applicationManager.GetIdAsync(application))!,
type: AuthorizationTypes.Permanent,
scopes: principal.GetScopes());
}
principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
case ConsentTypes.Explicit when request.HasPrompt(Prompts.None):
case ConsentTypes.Systematic when request.HasPrompt(Prompts.None):
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"Interactive user consent is required."
}!));
default:
return View(new AuthorizeViewModel
{
ApplicationName = await _applicationManager.GetDisplayNameAsync(application),
Scope = request.Scope
});
}
}
[Authorize, FormValueRequired("submit.Accept")]
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
var user = await _userManager.GetUserAsync(User) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
var authorizations = await _authorizationManager.FindAsync(
subject: await _userManager.GetUserIdAsync(user),
client: (await _applicationManager.GetIdAsync(application))!,
status: Statuses.Valid,
type: AuthorizationTypes.Permanent,
scopes: request.GetScopes()).ToListAsync();
if (!authorizations.Any() && await _applicationManager.HasConsentTypeAsync(application, ConsentTypes.External))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}!));
}
var principal = await _signInManager.CreateUserPrincipalAsync(user);
principal.SetScopes(request.GetScopes());
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
var authorization = authorizations.LastOrDefault();
if (authorization == null)
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
subject: await _userManager.GetUserIdAsync(user),
client: (await _applicationManager.GetIdAsync(application))!,
type: AuthorizationTypes.Permanent,
scopes: principal.GetScopes());
}
principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
UPDATE
My nginx configuration for better understading of the hosting environment:
server {
listen 9999 ssl;
server_name example.com;
ssl_certificate /etc/../certificate.crt;
ssl_certificate_key /etc/../key.key;
location / {
proxy_pass https://example.com:10000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwared-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Update 2
I'm still facing this issue. After some more testing & guessing around, I found out that the redirect to the registration page is correctly working by setting options.AcuthenticationPaths.RemoteRegisterPath in the client's program.cs to the absolute URL. There is no property to set the login path to its absolute URL, only LogInPath but no RemoteLogInPath like it's used for the registration page.
Any advice on what I am missing or getting wrong is greatly appreciated.