Hello. We’re attempting to change the authentication from our Umbraco 17 ASP.NET Core web site from Azure AD B2C to Amazon Cognito. As far as we can tell, Amazon Cognito is configured correctly, and we are able to authenticate via the login screen. However, the application seems to be caught in a loop, where it keeps refreshing and the ufprt token in the URL continues to grow until the web page throws an error stating the HTTP request is too large. Is anyone able to provide any guidance on how we get past this issue? I am posting what I consider to be the relevant code but let me know if you would like any more information. Thanks.
//Program.cs
// using statements
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
builder.Services
.AddDbContext<MemberServicesContext>()
.AddScoped<INotificationRepository, NotificationRepository>()
.AddScoped<INotificationService, NotificationService>()
.AddBugsnag(configuration =>
{
configuration.ApiKey = config["BugsnagApiKey"] ?? string.Empty;
configuration.ReleaseStage = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty;
});
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddOpenIdConnectAuthentication(config)
.AddServices(config)
.AddWebsite()
.AddDeliveryApi()
.AddComposers()
.SetCustomMemberLoginPath()
.Build();
WebApplication app = builder.Build();
await app.BootUmbracoAsync();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseAuthentication();
app.UseAuthorization();
app.UseXfo(options => options.SameOrigin());
app.UseUmbraco()
.WithMiddleware(u =>
{
u.UseBackOffice();
u.UseWebsite();
})
.WithEndpoints(u =>
{
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
app.Use(async (context, next) =>
{
context.Response.Headers?.Append("X-Content-Type-Options", "nosniff");
context.Response.Headers?.Append("X-Frame-Options", "SAMEORIGIN");
await next();
});
await app.RunAsync();
// ServiceExtensions.cs
// using statements
namespace OurApp;
public static class ServiceExtensions
{
public static IUmbracoBuilder AddOpenIdConnectAuthentication(this IUmbracoBuilder builder, IConfiguration config)
{
builder.Services.ConfigureOptions<OpenIdConnectMemberExternalLoginProviderOptions>();
builder.Services.AddSingleton<MemberCookieManager>();
builder.Services.ConfigureOptions<CustomCookieAuthenticationOptions>();
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<ITicketStore, MemoryCacheTicketStore>();
builder.AddMemberExternalLogins(logins =>
{
logins.AddMemberLogin(
memberAuthenticationBuilder =>
{
memberAuthenticationBuilder.AddOpenIdConnect(
memberAuthenticationBuilder.SchemeForMembers("UmbracoMembers." + "OpenIdConnect." + config["OpenIdConnectMember:AuthSchemeName"]),
options =>
{
//// pass configured options along
options.MetadataAddress = config["OpenIdConnectMember:MetadataAddress"];
options.ClientId = config["OpenIdConnectMember:ClientId"];
options.ClientSecret = config["OpenIdConnectMember:ClientSecret"];
options.CallbackPath = config["OpenIdConnectMember:CallbackPath"];
options.RemoteSignOutPath = config["OpenIdConnectMember:RemoteSignOutPath"];
options.Authority = config["OpenIdConnectMember:Authority"];
options.ClaimsIssuer = config["OpenIdConnectMember:IssuerUrl"];
string? signedOutRedirectUri = config["OpenIdConnectMember:SignedOutRedirectUri"];
if (signedOutRedirectUri != null)
{
options.SignedOutRedirectUri = signedOutRedirectUri;
}
// Use the authorization code flow
options.ResponseType = "code";
// map claims
options.TokenValidationParameters.NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
options.TokenValidationParameters.RoleClaimType = "role";
options.RequireHttpsMetadata = true;
options.SaveTokens = true;
string[] scopes = config["OpenIdConnectMember:Scopes"]?.Split(' ') ?? [];
foreach (string scope in scopes)
{
options.Scope.Add(scope);
}
options.UsePkce = true;
options.Events.OnRedirectToIdentityProvider = context =>
{
return Task.CompletedTask;
};
options.Events.OnTokenValidated = async context =>
{
var claims = context?.Principal?.Claims.ToList();
var email = (claims?.SingleOrDefault(x => x.Type == ClaimTypes.Email))
?? throw new Exception("Email claim is required for auto linking, but was not found.");
var name = claims?.SingleOrDefault(x => x.Type == "name");
var nameTypeClaim = claims?.SingleOrDefault(x => x.Type == ClaimTypes.Name);
if (name != null && name.Value != nameTypeClaim?.Value)
{
// The name claim is required for auto linking.
// So get it from another claim and put it in the name claim.
if (nameTypeClaim != null)
{
claims?.Remove(nameTypeClaim);
}
claims?.Add(new Claim(ClaimTypes.Name, name.Value));
if (context != null)
{
// Since we added new claims create a new principal.
var authenticationType = context.Principal?.Identity?.AuthenticationType;
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType));
}
}
await Task.FromResult(0);
};
options.Events.OnRemoteSignOut = context =>
{
return Task.CompletedTask;
};
options.Events.OnRemoteFailure = context =>
{
context.Response.Redirect($"/");
context.HandleResponse();
return Task.CompletedTask;
};
options.Events.OnSignedOutCallbackRedirect = context =>
{
return Task.FromResult(true);
};
options.Events.OnRedirectToIdentityProviderForSignOut = notification =>
{
return Task.CompletedTask;
};
});
});
});
return builder;
}
public static IUmbracoBuilder AddServices(this IUmbracoBuilder builder, IConfiguration config)
{
builder.Services.AddTransient<ISearchService, SearchService>();
return builder;
}
}
// OpenIdConnectMemberExternalLoginProviderOptions.cs
// using statements
namespace MyApp;
public class OpenIdConnectMemberExternalLoginProviderOptions : IConfigureNamedOptions<MemberExternalLoginProviderOptions>
{
private readonly ILogger<OpenIdConnectMemberExternalLoginProviderOptions> _logger;
private readonly IConfiguration _config;
public string? _schemeName;
public OpenIdConnectMemberExternalLoginProviderOptions(ILogger<OpenIdConnectMemberExternalLoginProviderOptions> logger, IConfiguration config)
{
_logger = logger;
_config = config;
_schemeName = _config["OpenIdConnectMember:AuthSchemeName"];
}
public void Configure(string? name, MemberExternalLoginProviderOptions options)
{
if (name != Constants.Security.MemberExternalAuthenticationTypePrefix + "OpenIdConnect." + _schemeName)
{
return;
}
Configure(options);
}
public void Configure(MemberExternalLoginProviderOptions options)
{
options.AutoLinkOptions = new MemberExternalSignInAutoLinkOptions(
autoLinkExternalAccount: true,
defaultCulture: null,
defaultIsApproved: true,
defaultMemberTypeAlias: "Member"
)
{
OnAutoLinking = (autoLinkUser, loginInfo) =>
{
OnAutoLinking(autoLinkUser, loginInfo);
},
OnExternalLogin = (user, loginInfo) =>
{
OnExternalLogin(user, loginInfo);
return true;
}
};
}
public void OnAutoLinking(MemberIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
{
string? jwtToken = loginInfo.AuthenticationTokens?.Where(t => t.Name == "access_token").Select(token => token.Value).FirstOrDefault();
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadToken(jwtToken);
var tokenFromIdentityProvider = (JwtSecurityToken)jsonToken;
var roles = tokenFromIdentityProvider.Claims.Where(c => c.Type == "user_roles");
autoLinkUser.Roles.Clear();
if (!roles.Any())
{
_logger.LogInformation("No user_roles found in user claims");
}
// add roles to autoLinkUser for each role in tokenFromIdentityProvider
}
public void OnExternalLogin(MemberIdentityUser user, ExternalLoginInfo loginInfo)
{
string? jwtToken = loginInfo.AuthenticationTokens?.Where(t => t.Name == "access_token").Select(token => token.Value).FirstOrDefault();
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadToken(jwtToken);
var tokenFromIdentityProvider = (JwtSecurityToken)jsonToken;
var roles = tokenFromIdentityProvider.Claims.Where(c => c.Type == "user_roles");
user.Roles.Clear();
// add roles to user for each role in tokenFromIdentityProvider
user.Claims.Clear();
// add claims to user for each claim in tokenFromIdentityProvider
}