Reputation: 3987
I am trying to set up 3 web applications as follows:
Gateway
is the identity server and uses IdentityServer4 v4.1.0
and runs on localhost:7443Web Frontend
is the web UI server, and renders a React
app, and runs on localhost:8443Backend
is the web API server and implements a GraphQL
API, and runs on localhost:9443All of the above are hosted in an ASP.NET Core 3.1
application.
I am sure I am making some newbie mistake, but when logging in via WebFrontend
I get the login page on the Gateway
, I can log in, but on the redirect, I get an HTTP 404 for /signin-oidc on the Gateway
.
This is what I get:
However, I can navigate to the login page on the Gateway
and log in.
What am I doing wrong?
The configuration is as follows:
Config.cs
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Json;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Test;
namespace Bakhtawar.Data
{
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId
{
DisplayName = "User identifier",
Description = "Your user identifier"
},
new IdentityResources.Profile
{
DisplayName = "User profile",
Description = "Your user profile information (first name, last name, etc.)"
},
new IdentityResources.Email
{
DisplayName = "User identifier",
Description = "Your user identifier"
}
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("bakhtawar.users"),
new ApiScope("bakhtawar.galleries"),
new ApiScope("bakhtawar.posts"),
new ApiScope("bakhtawar.comments")
};
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "bakhtawar.api",
ClientName = "Bakhtawar API",
ClientSecrets =
{
new Secret("893bfc0b-880c-4f5e-b258-41d007e08860".Sha256())
},
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes =
{
"bakhtawar.users",
"bakhtawar.galleries",
"bakhtawar.posts",
"bakhtawar.comments"
}
},
new Client
{
ClientId = "bakhtawar.web",
ClientName = "Bakhtawar Web",
ClientSecrets =
{
new Secret("ca39181f-12ce-4a44-a4fd-0955b39c4953".Sha256())
},
AllowedGrantTypes = GrantTypes.Code,
RedirectUris =
{
"https://localhost:7443/signin-oidc"
},
FrontChannelLogoutUri = "https://localhost:7443/signout-oidc",
PostLogoutRedirectUris =
{
"https://localhost:7443/signout-callback-oidc"
},
AllowedScopes =
{
"openid",
"profile",
"email",
"bakhtawar.users",
"bakhtawar.galleries",
"bakhtawar.posts",
"bakhtawar.comments"
},
AllowOfflineAccess = true,
RequireClientSecret = false
},
new Client
{
ClientId = "bakhtawar.app",
ClientName = "Bakhtawar App",
ClientSecrets =
{
new Secret("bd785003-0cce-4a77-9fec-516f033e3043".Sha256())
},
RedirectUris = { "urn:ietf:wg:oauth:2.0:oob" },
PostLogoutRedirectUris = { "https://notused" },
RequireClientSecret = false,
AllowedGrantTypes = GrantTypes.Code,
AllowedScopes =
{
"openid",
"profile",
"email",
"bakhtawar.users",
"bakhtawar.galleries",
"bakhtawar.posts",
"bakhtawar.comments"
},
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
RefreshTokenExpiration = TokenExpiration.Sliding
}
};
}
}
The Startup.cs
files for each of them are as follows:
Gateway/Startup.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Bakhtawar.Apps.GatewayApp.Contracts;
using Bakhtawar.Apps.GatewayApp.Services;
using Bakhtawar.Data;
using Bakhtawar.Models;
using IdentityServer4;
using IdentityServer4.Services;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Slugify;
namespace Bakhtawar.Apps.GatewayApp
{
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
{
Configuration = configuration;
Environment = environment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; }
public void ConfigureServices(IServiceCollection services)
{
services
.AddDbContext<IdentityDbContext>
(
(builder) => builder.UseSqlServer(Configuration["ConnectionStrings:Identity"], b => b.MigrationsAssembly("Bakhtawar.Apps.GatewayApp"))
);
services
.AddIdentity<User, Role>
(
(options) =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequireNonAlphanumeric = false;
}
)
.AddDefaultTokenProviders();
services
.AddSingleton<EFUserStore>()
.AddSingleton<EFRoleStore>();
services
.AddSingleton<IUserStore<User>>((serviceProvider) => serviceProvider.GetService<EFUserStore>())
.AddSingleton<IUserEmailStore<User>>((serviceProvider) => serviceProvider.GetService<EFUserStore>())
.AddSingleton<IUserPhoneNumberStore<User>>((serviceProvider) => serviceProvider.GetService<EFUserStore>())
.AddSingleton<IUserPasswordStore<User>>((serviceProvider) => serviceProvider.GetService<EFUserStore>())
.AddSingleton<IUserLoginStore<User>>((serviceProvider) => serviceProvider.GetService<EFUserStore>())
.AddSingleton<IUserLockoutStore<User>>((serviceProvider) => serviceProvider.GetService<EFUserStore>());
services
.AddSingleton<IRoleStore<Role>>((serviceProvider) => serviceProvider.GetService<EFRoleStore>());
services
.AddIdentityServer
(
(options) =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseSuccessEvents = true;
// HINT : see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
}
)
.AddAspNetIdentity<User>()
// NOTE : adds the config data from DB (clients, resources, CORS)
.AddConfigurationStore
(
(options) =>
{
options.ConfigureDbContext = (builder) => builder.UseSqlServer(Configuration["ConnectionStrings:Identity"], b => b.MigrationsAssembly("Bakhtawar.Apps.GatewayApp"));
}
)
// NOTE : adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore
(
(options) =>
{
options.ConfigureDbContext = (builder) => builder.UseSqlServer(Configuration["ConnectionStrings:Identity"], b => b.MigrationsAssembly("Bakhtawar.Apps.GatewayApp"));
// NOTE: enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
}
)
.AddServices
(
Environment.IsDevelopment(),
(builder) => builder.AddDeveloperSigningCredential(false),
(builder) => builder.AddSigningCredential
(
new X509Certificate2
(
File.ReadAllBytes(Configuration["IdentityServer:Key:FilePath"]),
(string) Configuration["IdentityServer:Key:Password"]
)
)
);
services
.AddAuthentication()
.AddCookie("Cookies")
.AddService
(
Configuration["Secret:Google:ClientId"] != null && Configuration["Secret:Google:ClientSecret"] != null,
(builder) => builder
.AddGoogle
(
"Google",
(options) =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// NOTE : register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to https://localhost:4443/signin-google & https://id.bakhtawar.co.uk/signin-google
options.ClientId = Configuration["Secret:Google:ClientId"];
options.ClientSecret = Configuration["Secret:Google:ClientSecret"];
}
)
)
.AddLocalApi
(
(options) =>
{
options.ExpectedScope = "api";
}
);
services
.AddOidcStateDataFormatterCache("aad");
services
.AddControllersWithViews();
services
.AddRazorPages();
services
.AddSameSiteCookiePolicy();
services
.AddCors
(
(options) =>
{
options.AddPolicy
(
"api",
(policy) =>
{
policy
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
}
);
}
);
services
.AddScoped<IUserProvisioner<User>, UserProvisioner>();
services
.AddScoped<IClientRequestParametersProvider, ClientRequestParametersProvider>();
services
.AddScoped<IAbsoluteUrlGenerator, AbsoluteUrlGenerator>();
services
.AddTransient<IPasswordValidator, PasswordValidator>();
services
.AddTransient<IRedirectUriValidator, DoNothingRedirectValidator>();
services
.AddTransient<ICorsPolicyService, DoNothingCorsPolicyService>();
services
.AddSingleton<IEmailSender, FileSystemEmailSender>();
services
.AddSingleton<SlugHelper>();
services.Configure<ForwardedHeadersOptions>
(
(options) =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto;
if (!Environment.IsDevelopment())
{
var knownNetworks = Configuration["ForwardedHeadersOptions:KnownNetworks"];
if (!string.IsNullOrEmpty(knownNetworks))
{
foreach (var knownNetwork in knownNetworks.Split(";"))
{
var parts = knownNetwork.Split(":");
var prefix = parts[0];
var prefixLength = int.Parse(parts[1]);
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse(prefix), prefixLength));
}
}
}
}
);
}
public void Configure(IApplicationBuilder app)
{
app.UseForwardedHeaders();
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCookiePolicy();
app.UseCors("api");
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints
(
(endpoints) =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapRazorPages();
}
);
}
}
public static class SameSiteCookiePolicyExtensions
{
public static IServiceCollection AddSameSiteCookiePolicy(this IServiceCollection services)
{
services.Configure<CookiePolicyOptions>
(
(options) =>
{
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
options.OnAppendCookie = (cookieContext) => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.OnDeleteCookie = (cookieContext) => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
}
);
return services;
}
private static void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
if (options.SameSite == SameSiteMode.None)
{
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
if (DisallowsSameSiteNone(userAgent))
{
// NOTE : for .NET Core < 3.1, set SameSite = (SameSiteMode)(-1)
options.SameSite = SameSiteMode.Unspecified;
}
}
}
private static bool DisallowsSameSiteNone(string userAgent)
{
// NOTE: cover all iOS-based browsers here. This includes:
// - Safari on iOS 12 for iPhone, iPod Touch, iPad
// - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
// - Chrome on iOS 12 for iPhone, iPod Touch, iPad
// All of which are broken by SameSite=None, because they use the iOS networking stack
if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12"))
{
return true;
}
// NOTE: cover Mac OS X based browsers that use the Mac OS networking stack. This includes:
// - Safari on Mac OS X.
// This does not include:
// - Chrome on Mac OS X
// Because they do not use the Mac OS networking stack.
if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") &&
userAgent.Contains("Version/") && userAgent.Contains("Safari"))
{
return true;
}
// NOTE : cover Chrome 50-69, because some versions are broken by SameSite=None,
// and none in this range require it.
// Note: this covers some pre-Chromium Edge versions,
// but pre-Chromium Edge does not require SameSite=None.
if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
{
return true;
}
return false;
}
}
public static class AuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddService(this AuthenticationBuilder builder, bool condition, Func<AuthenticationBuilder, AuthenticationBuilder> build)
{
if (condition)
{
builder = build(builder);
}
return builder;
}
public static AuthenticationBuilder AddService(this AuthenticationBuilder builder, Func<bool> condition, Func<AuthenticationBuilder, AuthenticationBuilder> build)
{
return builder.AddService(condition(), build);
}
}
public static class IdentityServerBuilderExtensions
{
public static IIdentityServerBuilder AddServices
(
this IIdentityServerBuilder identityServerBuilder,
Func<bool> condition,
Func<IIdentityServerBuilder, IIdentityServerBuilder> ifTrue,
Func<IIdentityServerBuilder, IIdentityServerBuilder> ifFalse
)
{
return identityServerBuilder.AddServices(condition(), ifTrue, ifFalse);
}
public static IIdentityServerBuilder AddServices
(
this IIdentityServerBuilder identityServerBuilder,
bool condition,
Func<IIdentityServerBuilder, IIdentityServerBuilder> ifTrue,
Func<IIdentityServerBuilder, IIdentityServerBuilder> ifFalse
)
{
if (condition)
{
return ifTrue(identityServerBuilder);
}
else
{
return ifFalse(identityServerBuilder);
}
}
}
}
WebFrontend/Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Bakhtawar.Data;
using Bakhtawar.Models;
using Bakhtawar.Services;
using IdentityServer4;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Bakhtawar.Apps.WebFrontendApp
{
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
{
Configuration = configuration;
Environment = environment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; }
public void ConfigureServices(IServiceCollection services)
{
services
.AddDbContext<IdentityDbContext>
(
(options) => { options.UseSqlServer(Configuration["ConnectionStrings:Identity"]); }
);
services
.AddAuthentication();
services
.AddControllersWithViews
(
(options) =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()));
}
);
services.AddRazorPages();
services
.AddSpaStaticFiles
(
(options) => { options.RootPath = "App/build"; }
);
services.Configure<ForwardedHeadersOptions>
(
(options) =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto;
if (!Environment.IsDevelopment())
{
var knownNetworks = Configuration["ForwardedHeadersOptions:KnownNetworks"];
if (!string.IsNullOrEmpty(knownNetworks))
{
foreach (var knownNetwork in knownNetworks.Split(";"))
{
var parts = knownNetwork.Split(":");
var prefix = parts[0];
var prefixLength = int.Parse(parts[1]);
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse(prefix), prefixLength));
}
}
}
}
);
}
public void Configure(IApplicationBuilder app)
{
app.UseForwardedHeaders();
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints
(
(endpoints) =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapRazorPages();
}
);
app.UseSpa
(
(spa) =>
{
spa.Options.SourcePath = "App";
if (Environment.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
}
);
}
}
public static class AuthenticationBuilderExtension
{
public static AuthenticationBuilder AddService(this AuthenticationBuilder builder, bool condition, Func<AuthenticationBuilder, AuthenticationBuilder> build)
{
if (condition)
{
builder = build(builder);
}
return builder;
}
}
}
Upvotes: 1
Views: 2211
Reputation: 19901
I think you need to set the IssuerUri [here][1] so that IdentityServer believes it is on the public domain of the gateway, the domain that the clients sees.
https://identityserver4.readthedocs.io/en/latest/reference/options.html
Upvotes: 0
Reputation: 2394
/signin-oidc
is the remote sign-in address for the OpenId Connect authentication handler. This route is is handled by OpenId Connect authentication middleware. This means if you dont have the OIDC middleware this route wouldn't exist.
If your MVC app's hosting the IDS4, you dont need to add a client config for it. If you need an MVC app, create a new MVC app.
Here I have a sample includes JsClient and also MVC client.
Upvotes: 1