I thought I would share the results of my tests thus far as it uncovered some things I really didn't expect. Maybe these things are obvious/known to most of you - but for me they definitely proved to be tricky.
The first important thing I learned is that you must be specific around the challenge types your app issues. If you are self-hosting a login page you need to use:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
If you are using the Okta (or other openId provider) login page, you need to issue a different type of challenge:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
What I learned is that if you're using OpenID Connect in a ASP.NET Core application, and you are self-hosting the login page, the flow kinda goes like this:
- User tries to access protected resource
- [Authorize] marker tells the Okta middleware to inject itself and do its thing. User.Identity.IsAuthenticated is false so it redirects to whatever is set under the cookie options as the login path:
.AddCookie(options =>
{
options.LoginPath = new PathString("/Account/SignIn");
options.Cookie.Name = ".AspNet.SharedCookie";
})
My login page is just using the Okta widget, so clicking login sends user to Okta, Okta does its thing, and then sends the user back to the app to the every mysterious "authorization-code/callback" url. That url is handled by the Okta middleware.
The middleware processes the tokens being sent from Okta, calls the /userinfo endpoint to get the claims (presuming you have that property set to true) and populates a ClaimsPrincipal.
Here is the part I missed What it also does is set a cookie(s). That cookie is what, from that point forward, the .net framework will be checking to determine who the user is and if they are authenticated. I completely misunderstood this. I thought that the middleware would be making calls to /authorize or something like that in the background.
So - the big lesson learned here for me is this: just because both apps are using the same OpenID server doesn't mean they can easily share auth/sessions. In order for true SSO to work seamlessly, they have to also share a cookie. And that brings me to my second discovery: sharing cookies across ASP.NET core applications. The short version is you have to:
a. Name the cookie the same in both apps
b. Use the IDataProtection interface to store your cookie keys somewhere both apps can get to (I chose Azure).
c. Make sure the cookie domain is the same for both apps
So at the end of the day, this is what my code looked like end-to-end (in each app's startup). I am still dealing with some oddities specific to Nopcommerce, but hopefully they will get sorted out here soon.
var oktaMvcOptions = new OktaMvcOptions()
{
OktaDomain = oktaDomain,
ClientId = clientId,
ClientSecret = clientSecret,
Scope = new List<string> { "openid", "profile", "email", "address", "groups" },
AuthorizationServerId = authServerId,
GetClaimsFromUserInfoEndpoint = true
};
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(storageConnectionString);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = blobClient.GetContainerReference(storageContainerName);
var blob = container.GetBlockBlobReference("somefilename.xml");
services.AddDataProtection().SetApplicationName("somesharedappname").PersistKeysToAzureBlobStorage(blob);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = new PathString("/Account/SignIn");
options.Cookie.Name = "somesharedcookiename";
})
.AddOktaMvc(oktaMvcOptions);
I hope this helps someone who ends up struggling with the same issues. If anyone can suggest a better mechanism for implementing this, I would love to hear your ideas and suggestions.