I have a weird issue, everytime I restart my IIS server or service (i.e. IISreset) my application goes into an almost infinite login loop. After about 15 minutes it does back to normal.
I'm using the default Visual studio Blazor server template with Azure AD module, I have a feeling it's to do with a server side token cache but I'm unsure. I have the same config running on an Azure Web app and I have no issues, here's my authentication from my Program.cs:
var builder = WebApplication.CreateBuilder(args);
var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ') ?? builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' ');
// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
.AddInMemoryTokenCaches()
.AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi"))
.AddDistributedTokenCaches();
IConfigurationSection SQL = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("SQL");
SqlConnectionStringBuilder SQLbuilder = new()
{
DataSource = SQL["DataSource"],
UserID = SQL["UserID"],
Password = SQL["Password"],
InitialCatalog = SQL["InitialCatalog"]
};
builder.Services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = SQLbuilder.ConnectionString;
options.SchemaName = "dbo";
options.TableName = "TokenCache";
});
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
Any ideas on what I'm missing?
Related
I've created a C# ASP .Net Core 6.0 application, and trying to implement SSO with Azure AD using Sustainsys.Saml2, specifically with the Sustainsys.Saml2.AspNetCore2 package. Having tested the implementation on my development machine with localhost, I can see it works as expected and authenticates the user, populates the Identity model, and redirects to correct URL.
However, when deployed into the test environment, using a dockerized version, the behaviour changes. When triggering SSO, the user is authenticated successfully in Azure, but when returning to the app, it returns an Error 500 at the Saml2/Acs endpoint. Reviewing the logs show no indication of any errors, and instead report successful authentication for the user.
The Program.cs configuration:
builder.Services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = "Saml2";
})
.AddSaml2(options =>
{
var logger = new LoggerFactory();
options.SPOptions.Logger = new AspNetCoreLoggerAdapter(logger.CreateLogger<Saml2Handler>());
options.SPOptions.EntityId = new EntityId(AppConfig.Saml_EntityID);
options.IdentityProviders.Add(
new IdentityProvider(new EntityId(AppConfig.Saml_AzureID), options.SPOptions)
{
Binding = Saml2BindingType.HttpRedirect,
LoadMetadata = true,
MetadataLocation = AppConfig.Saml_Metadata,
DisableOutboundLogoutRequests = false,
AllowUnsolicitedAuthnResponse = true
});
options.SPOptions.PublicOrigin = new Uri(AppConfig.BaseUrl);
options.SPOptions.ReturnUrl = new Uri(AppConfig.BaseUrl);
options.SPOptions.WantAssertionsSigned = true;
options.SPOptions.AuthenticateRequestSigningBehavior = SigningBehavior.Always;
options.SPOptions.ServiceCertificates.Add(new X509Certificate2(AppConfig.Saml_Cert_Path));
})
.AddCookie();
While troubleshooting the issue, I stumbled across some confusing behaviour that may or may not indicate what may be the issue. If I follow the following steps, I can end up at a point where the user is authenticated and can use the applications:
Click 'Login' to trigger the Saml authentication.
Hit the Error 500 at Saml2/Acs.
Click 'refresh', and 'continue' to resubmit the request.
The browser then continues to the intended URL, but says 'Connection Refused'
Use the browser back buttons to return to the application home screen, and refresh the page... Viola! Logged in!
Furthermore, when inspecting the request headers on the Saml2/Acs endpoint, I can see a Saml response is returned, which I can manually decode from base64 and read the correct information!
As mentioned, the logs don't mention any errors, just:
Initiating login to https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/
and
Successfully processed SAML response _ba082bb8-7d2c-4aa4-a7dc-b1520312d084 and authenticated a*******#********.com
Any assistance, or guidance to a resolution would be much appreciated!
Maybe this is not relevant for .Net Core, but for .net framework 4.8
there was the following issues:
ReturnUrl of Service Provider was wrong: http://locahost/mysite/saml2/acs instead of correct one http://locahost/mysite/ (with trailing slash). Because of this, there was indefinite loop to http://locahost/mysite/saml2/acs`.
SPOptions spOptions = new SPOptions() { EntityId = new EntityId(spMetadataUrl), ReturnUrl = new Uri(hostUrl + "/"), DiscoveryServiceUrl = new Uri(hostUrl + #"/DiscoveryService"), Organization = organization, AuthenticateRequestSigningBehavior = SigningBehavior.Never, RequestedAuthnContext = requestedAuthnContext, Logger = logger, PublicOrigin = hostUri };
DO NOT USE UseExternalSignInCookie mehod, otherwise ClaimPrincipal will not set for current Thread (cookies will be parsed to claims, although latter will not be set, this can be checked with code below):
Saml2AuthenticationOptions options = CreateSaml2Options(configuration, certificate); options.SPOptions.Saml2PSecurityTokenHandler = new MySaml2PSecurityTokenHandler();
public class MySaml2PSecurityTokenHandler : Sustainsys.Saml2.Saml2P.Saml2PSecurityTokenHandler
{
protected override ClaimsIdentity CreateClaimsIdentity(Saml2SecurityToken samlToken, string issuer, TokenValidationParameters validationParameters)
{
ClaimsIdentity identity = base.CreateClaimsIdentity(samlToken, issuer, validationParameters);
Claim claim = new Claim("Name", "jon.doe");
Claim[] claims = new Claim[] { claim };
identity.AddClaims(claims);
return identity;
}
}
Additionally only for Net Framework 4.8, because of owin vs System.Web cookies bug, CookieManager should be used. Code:
var cookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebCookieManager();
Saml2AuthenticationOptions options = CreateSaml2Options(configuration, certificate);
CookieAuthenticationOptions cookieAuthentication = new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString(configuration.ServiceProviderSignOnUrl),
CookieManager = ŃookieManager,
ReturnUrlParameter = GetBase() + "/",
Provider = new CookieAuthenticationProvider()
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = (context) =>
{
var newIdentity = new ClaimsIdentity(context.Identity);
int newcount = 1;
newIdentity.AddClaim(new Claim("SIMPLECOUNT", newcount.ToString()));
context.ReplaceIdentity(newIdentity);
return Task.FromResult<object>(null);
}
}
};
app.UseCookieAuthentication(cookieAuthentication);
app.SetDefaultSignInAsAuthenticationType(cookieAuthentication.AuthenticationType);
//app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseSaml2Authentication(options);
I'm getting the following exception when attempting to browse to my site :
The configuration/staticcontent endpoint is implemented like so:
[HttpGet("staticcontent")]
public async Task<IActionResult> GetStaticContent()
{
return Ok(this.mapper.Map<StaticContentValueDto[]>(await this.staticContentValuesProvider.GetStaticContentValues()));
}
.. .and the implementation of GetStaticContentValues() as follows:
public async Task<IEnumerable<StaticContentValue>> GetStaticContentValues()
{
return await this.dbContext.StaticContentValues.ToArrayAsync();
}
I suspect there might be an issue with AD authentication?
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry();
services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; });
services.AddMvcCore()
.AddAuthorization();
services.AddControllers().AddNewtonsoftJson(o=>
{
o.SerializerSettings.ContractResolver = new DefaultContractResolver();
o.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
string connection = Configuration.GetConnectionString("DefaultConnection");
string reportingConnection = Configuration.GetConnectionString("ReportingConnection");
services.AddDbContext<PnbIdentityDbContext>(options =>
options.UseSqlServer(connection));
//20 or so other adddbcontext for sql server here
//20 or so other adddbcontext for sql server here
services.AddIdentity<PnbIdentityUser, PnbIdentityRole>(options => {
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
})
.AddEntityFrameworkStores<PnbIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddAutoMapper(typeof(CapsAutoMapperProfile));
SessionConfigurator.Configure(services);
AuthConfigurator.Configure(services, Configuration["Identity:TokenSecret"]);
DiConfigurator.Configure(services);
HangfireConfigurator.Configure(services, connection);
}
Please note that Identity:TokenSecret is set in appsettings.json.
What am I doing wrong? What is the reason for the 401 response?
any chance you can point me to where i can configure authentication ?
Thank you Ken W MSFT , Posting your suggestion as answer to help other community members.
"Yes, it is possible. Azure Static Web Apps provides a streamlined authentication experience. By default, you have access to a series of pre-configured providers, or the option to register a custom provider.
Any user can authenticate with an enabled provider. Once logged in, users belong to the anonymous and authenticated roles by default.
Authorized users gain access to restricted routes by rules defined in the staticwebapp.config.json file. Users join custom roles via provider-specific invitations, or through a custom Azure Active
Directory provider registration.
All authentication providers are enabled by default.
To restrict an authentication provider, block access with a custom route rule.
Pre-configured providers include:
Azure Active Directory
GitHub
Twitter
Reference:
https://learn.microsoft.com/en-us/azure/static-web-apps/authentication-authorization ''
i have an issue with my c# blazor server setup.
I am fairly new to the .net eco system and I have a problem I can not figure out on my own. I hope any of you can help me.
I have a blazor server setup with Azure AD Authentication:
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
When the user has autheticated, I can get an access token with the following code:
var token = new TokenProvider
{
AccessToken = await HttpContext.GetTokenAsync("access_token")
};
I use this token to call the Microsoft Web API to fetch customers from Dynamics CRM, with the following setup:
// Setup client
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme,
opt =>
{
var resourceUri = new Uri(Configuration["CDSAPI"]);
var resource = $"{resourceUri.Scheme}://{resourceUri.Host}/";
opt.ResponseType = "code";
opt.SaveTokens = true;
opt.Scope.Add("user_impersonation");
opt.Scope.Add(new Uri(Configuration["CDSAPI"]).Host);
opt.Resource = resource;
});
services.AddScoped<TokenProvider>();
services.AddHttpClient("CDS", client =>
{
client.BaseAddress = new Uri(Configuration["CDSAPI"]);
});
I then use this client in my code to fetch the customers:
try
{
var client = Factory.CreateClient("CDS");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", TokenProvider.AccessToken);
var result = await client.GetStringAsync("accounts?$select=name&$orderby=name");
DynamicsCustomerEntity customerCollection = JsonConvert.DeserializeObject<DynamicsCustomerEntity>(result);
customers = customerCollection.value;
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
finally
{
}
This works fine. But the issue presents itself when I try to implement Microsoft Graph functionality.
For example, if I try to use this code:
services.AddMicrosoftWebAppAuthentication(Configuration)
.AddMicrosoftWebAppCallsWebApi(Configuration, new string[] { "Calendars.Read", "Calendars.ReadWrite" })
.AddDistributedTokenCaches();
services.AddHttpClient();
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
instead of
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
I can no longer access the token with
await HttpContext.GetTokenAsync("access_token")
So my question is:
How can I make a blazor server setup to get two valid tokens - one for Graph API calls and one for Microsoft Web API calls? I need delegated permission tokens for both.
I hope my question makes sence. I have tried to provide as much context as possible.
Thanks in advance.
I am in the process of integrating a simplefied authentication process into a asp.net core 2.1 application, where users are logging in via the UI by default, but there is also the possibility to aquire a token and call some secured api endpoints to retrieve some data needed for reporting.
The issue I am facing is, that with the default configuration everything works, but adding the token config throws some weird errors.
If I do not add AddCookie("Identity.External"), call to the onGet method at /Identity/Account/Login throws the exception
InvalidOperationException: No sign-out authentication handler is registered for the scheme 'Identity.External'. The registered sign-out schemes are: Identity.Application. Did you forget to call AddAuthentication().AddCookies("Identity.External",...)?
If I do not specify options.DefaultScheme = "Identity.Application"; the user is not successfully signed in.
If I do not add .AddCookie("Identity.External") and .AddCookie("Identity.TwoFactorUserId") the logout process throws the same exception as above.
For the login process, this is simply rectified by removing the line await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);. If I do not use external schemes I do not need to sign out of them, right?
This brings me to my problem: How can I disable external logins and multi factor authentication in Identity Core, so I do not have to add those cookies in the first place? Furthermore, why do I have to specifiy a cookie named "Identity.Application", which is not the case in the default configuration? I'm pretty sure this is just another issue of me not thoroughly understanding the problem at hand, so I am grateful for any clarification on this.
This is my Identity config from the Startup.cs I have also scaffolded out the complete Identity UI with a custom IdentityUser class.
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});
var tokenValidationParameters = new TokenValidationParameters
{
/*...*/
};
services.AddAuthentication(options =>
{
options.DefaultScheme = "Identity.Application";
})
//.AddCookie("Identity.External")
//.AddCookie("Identity.TwoFactorUserId")
.AddCookie("Identity.Application", opt =>
{
opt.SlidingExpiration = true;
})
.AddJwtBearer(options =>
{
options.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.TokenValidationParameters = tokenValidationParameters;
options.SaveToken = true;
});
var builder = services.AddIdentityCore<AppUser>(o =>
{
//removed
});
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
builder.AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
I'd like to add support for administrators to add a trusted federation to my site. I'm trying to do this in my controller without having to restart the web app using:
app.Map("/Account", configuration =>
{
var provider = new WsFederationAuthenticationOptions
{
AuthenticationType = organizationModel.ADFS_Domain,
MetadataAddress = organizationModel.ADFS_MetadataAddress,
BackchannelCertificateValidator = null,
Wtrealm = organizationModel.ADFS_Realm,
Wreply = serveraddress + " /Account/" + Guid.NewGuid().ToString(),
};
configuration.UseWsFederationAuthentication(provider);
});
The code above executes without errors, but the provider is not added to the Owin context. Calling...
HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes()
does not include the new option... :(
Just to clarify, I can do the same thing in my Startup.Auth.cs, but again, I don't want to restart the host...
Any ideas?