The options.SignInScheme needs to be ADFS judging by your config.
options.SignInScheme = "ADFS";
In your old config you have the scopes
Scope = "openid profile allatclaims",
While in the new config you only do
options.Scope.Add("openid");
This may or may not be an issue, but typically dont send scopes unless you care for them in your provider. You're going to need to ensure that that options.CallbackPath is correct, but begin by checking the above.
Oh and you should still implement the ISynchronizingUserService when signing in like you do in your old sample.
options.Events.OnTokenValidated = (ctx) =>
{
// maybe not needed
var redirectUri = new Uri(ctx.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
if (redirectUri.IsAbsoluteUri)
{
ctx.Properties.RedirectUri = redirectUri.PathAndQuery;
}
//Sync user and the roles to EPiServer in the background
ServiceLocator.Current.GetInstance<ISynchronizingUserService>()
.SynchronizeAsync(ctx.Principal.Identity as ClaimsIdentity);
return Task.FromResult(0);
};
Hi Eric,
I think I mentioned in one ofthe comments in the code that scope has been altered in many different ways. It was set to original version (cms 11) and then to various values with and without allatclaims. Everytime it was tihe same result => user didn't get authenticated.
I forgot to mention that synchronizing user service was present in the OnTokenValiedated and moreover it was wrapped in try-catch block. It never did throw exception but the process ended in OnAuthenticationFailed event nevertheless.
Once I tried to remove all the scope (as that was suggested at some point) and got 'invalid_grant' error message.
I am guessing that it's some very trivial mistake at my code that causes this strange behavior.
Just noticed that you have declared options.Events.OnRedirectToIdentityProvider twice, that may or may not cause this behaviour.
I'd probably drop the options.Resource parameter as well, not very common to use and may cause various issues.
Do you really need to use options.UsePkce = true?
Other than that (which I'm sure you already checked)
Client decided to upgrade to CMS 12 and by doing so moving away from ADFS to Entra ID login. Trick here is that it's a public service and some of the branches will continue to use ADFS as a mean to login into CMS untill they move their solution to Entra ID too. That leaves me with a problem of enabling both Entra ID and ADFS working in paralell for some period of time.
I have managed to implement Entra part but got stuck with converting ADFS to work.
Here is the patch that works in CMS 11:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Scope = "openid profile allatclaims",
MetadataAddress = ConfigurationManager.AppSettings["ADFS_MetadataAddress"],
ClientId = ConfigurationManager.AppSettings["ADFS_ClientId"],
Resource = ConfigurationManager.AppSettings["ADFS_RelyingPartyIdentifier"],
ClientSecret = ConfigurationManager.AppSettings["ADFS_ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["ADFS_RedirectUri"],
PostLogoutRedirectUri = ConfigurationManager.AppSettings["ADFS_PostLogoutRedirectUri"],
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Write(context.Exception.Message);
return Task.FromResult(0);
},
RedirectToIdentityProvider = context =>
{
// To avoid a redirect loop to the federation server send 403
// when user is authenticated but does not have access
if (context.OwinContext.Response.StatusCode == 401 && context.OwinContext.Authentication.User.Identity.IsAuthenticated)
{
context.OwinContext.Response.StatusCode = 403;
context.HandleResponse();
}
//XHR requests cannot handle redirects to a login screen, return 401
if (context.OwinContext.Response.StatusCode == 401 && IsXhrRequest(context.OwinContext.Request))
context.HandleResponse();
return Task.FromResult(0);
},
SecurityTokenValidated = ctx =>
{
ctx.AuthenticationTicket.Properties.AllowRefresh = true;
//Ignore scheme/host name in redirect Uri to make sure a redirect to HTTPS does not redirect back to HTTP
var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
if (redirectUri.IsAbsoluteUri)
{
var queryStringDict = System.Web.HttpUtility.ParseQueryString(redirectUri.Query);
ctx.AuthenticationTicket.Properties.RedirectUri = queryStringDict.Get("returnUrl");
}
if (ctx.AuthenticationTicket.Identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Email) == null )
{
var upnClaim = ctx.AuthenticationTicket.Identity.Claims.FirstOrDefault(y => y.Type == ClaimTypes.Upn);
if (upnClaim != null && upnClaim.Value.Contains("@"))
{
ctx.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Email, upnClaim.Value));
}
}
//Sync user and the roles to EPiServer in the background
ServiceLocator.Current.GetInstance<ISynchronizingUserService>().SynchronizeAsync(ctx.AuthenticationTicket.Identity);
return Task.FromResult(0);
}
}
});
//Add stage marker to make sure WsFederation runs on Authenticate (before URL Authorization and virtual roles)
app.UseStageMarker(PipelineStage.Authenticate);
And here is my attempt to convert that to OpenIdConnect.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "policy-scheme";
options.DefaultScheme = "policy-scheme";
options.DefaultChallengeScheme = "policy-scheme";
})
.AddCookie("local-cookie", options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(1); // Adjust local login session timeout
options.SlidingExpiration = true;
options.LoginPath = "/episerver/cms";
})
.AddCookie("azure-cookie", options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(1); // Adjust local login session timeout
options.SlidingExpiration = true;
options.Events.OnSignedIn = async ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
{
//Syncs user and roles so they are available to the CMS
var syncService = ctx
.HttpContext
.RequestServices
.GetRequiredService<ISynchronizingUserService>();
await syncService.SynchronizeAsync(claimsIdentity);
}
};
options.Cookie.SameSite = SameSiteMode.None;
})
//there is additional directive .AddOpenIdConnect(for-entra-id) that is ommitted here for simplicity
.AddOpenIdConnect("ADFS", "ADFS", options =>
{
options.SignInScheme = "azure-cookie";
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.CallbackPath = "/episerver/cms";//I changed this so many times in in collaboration with IT personel w/o success
options.UsePkce = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.MetadataAddress = _configuration["ADFS_MetadataAddress"];
options.ClientId = _configuration["ADFS_ClientId"];
options.Resource = _configuration["ADFS_RelyingPartyIdentifier"];
options.ClientSecret = _configuration["ADFS_ClientSecret"]; // here is the actual secret value and not the cliend Id
options.SkipUnrecognizedRequests = true;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "unique_name",
RoleClaimType = "roles",
ValidateIssuer = false
};
options.MapInboundClaims = false;
options.Events.OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.RedirectUri = _configuration["ADFS_RedirectUri"];
context.ProtocolMessage.PostLogoutRedirectUri = _configuration["ADFS_PostLogoutRedirectUri"];
return Task.CompletedTask;
};
options.Events.OnTokenValidated = async context =>
{
//context.Principal here is authenticated.. context object also contains all the necessary roles
await Task.CompletedTask;
};
options.Events.OnAuthenticationFailed = context =>
{
// for some reason the flow ends up here without getting to .OnSignedIn or .AddCookie
context.HandleResponse();
context.Response.OnStarting(async () =>
{
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(context.Exception.Message);
});
return Task.CompletedTask;
};
options.Events.OnRedirectToIdentityProvider = context =>
{
// To avoid a redirect loop to the federation server send 403
// when user is authenticated but does not have access
if (context.Response.StatusCode == 401 && (context.HttpContext?.User?.Identity?.IsAuthenticated ?? false))
{
context.Response.StatusCode = 403;
context.HandleResponse();
}
//XHR requests cannot handle redirects to a login screen, return 401
if (context.Response.StatusCode == 401 && IsXhrRequest(context.Request))
context.HandleResponse();
return Task.FromResult(0);
};
})
.AddPolicyScheme("policy-scheme", null, options =>
{
options.ForwardDefaultSelector = ctx =>
{
if (ctx.Request.Cookies.ContainsKey(".AspNetCore." + IdentityConstants.ApplicationScheme))
{
return IdentityConstants.ApplicationScheme;
}
// this is a bit simplified also
return ctx.Request.Cookies.ContainsKey(".AspNetCore.azure-cookie")
? OpenIdConnectDefaults.AuthenticationScheme
: "ADFS";
};
});
The thing is that when request gets challenged user gets redirected to ADFS login portal. Authentication flow returns to my piece of code in the OnTokenValidated and I can confirm that authority responded OK 200 with Principal being authenticated with all the proper roles asigned to it but for some reasons flow ends up in OnAuthenticaionFailed event handler.
IT personel did debug on their side what gets sent to server and what I should receive but I cannot see what could be the reason for failing authentication on my side.