.Net Core Web API Basic Authentication Authorize does not work on Azure - azure

I wrote a custom auth handler for a web API in .net core 3.0 following this tutorial by Jason Watmore. The Authorize attribute works great IIS express. However, when I publish the code to Azure Web App the Authorize attribute does not fire. There is no authentication challenge and data is returned without authentication.
Azure Authentication Authorization Settings
Here is the custom BasicAuthenticationHandler
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IAPIRepo _apiRepo;
public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder,
ISystemClock clock,
IAPIRepo apiRepo): base(options, logger, encoder, clock)
{
_apiRepo = apiRepo;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//throw new NotImplementedException();
if (!Request.Headers.ContainsKey("Authorization"))
return AuthenticateResult.Fail("Missing Authorization Header");
User user = null;
try
{
var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
var username = credentials[0];
var password = credentials[1];
user = _apiRepo.Authenticate(username, password);
}
catch
{
return AuthenticateResult.Fail("Invalid Authorization Header");
}
if (user == null)
return AuthenticateResult.Fail("Invalid Username or Password");
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, user.User_Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
Startup.cs
services.AddScoped<IAPIRepo, APIRepo>();
services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Edit: Difference between .net core 2.2 and 3.1. Changing the run time to 3.1 fixed the issue

It looks like you are using the Startup.cs of .NET Core 3.0 instead of 3.1 like the article is using.

Related

Identityserver4 windows authentication failed on IIS but working in VS

I am trying to implement Identityserver4 (version 4.0.0) with windows authentication. While running on visual studio its working correctly. When I deploy this to IIS windows popup is showing continuously (401 status) after entering credentials. Below is my code . I also tried to deploy Duende Software's sample source also but getting the same result. I think there is some configuration missing from my end. Kindly help me.
Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
launchSettings.json
"windowsAuthentication": true,
ExternalController.cs
public async Task<IActionResult> Challenge(string scheme, string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
if(scheme == "Windows")
{
return await ChallengeWindowsAsync(returnUrl);
}
// validate returnUrl - either it is a valid OIDC URL or back to a local page
if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
// start challenge and roundtrip the return URL and scheme
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", scheme },
}
};
return Challenge(props, scheme);
}
//ChallengeWindowsAsync
private async Task<IActionResult> ChallengeWindowsAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync("Windows");
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, treating windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", "Windows" },
}
};
var id = new ClaimsIdentity("Windows");
// the sid is a good sub value
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.FindFirst(ClaimTypes.PrimarySid).Value));
// the account name is the closest we have to a display name
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
// add the groups as claims -- be careful if the number of groups is too large
var wi = wp.Identity as WindowsIdentity;
// translate group SIDs to display names
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
await HttpContext.SignInAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
return Redirect(props.RedirectUri);
}
else
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge("Windows");
}
}
IIS Configuration
Windows authentication is enabled

Azure SignalR Services with Bearer Authentication

We are trying to scale out our calendar application, that uses SignalR to push updates to clients based on their user's OrganizationId. Previously, the SignalR stuff was hosted within the single App Server, but to make it work across multiple servers we have opted to use Azure SignalR Services.
However, when the application uses the Azure solution, autorisation breaks.
Authentication is set up in Startup.cs to look for the token in the url/query-string when dealing with Hub endpoints:
//From: Startup.cs (abridged)
public IServiceProvider ConfigureServices(IServiceCollection services)
var authenticationBuilder = services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = OAuthValidationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OAuthValidationDefaults.AuthenticationScheme;
});
authenticationBuilder
.AddOAuthValidation(options => {
options.Events.OnRetrieveToken = async context => {
// Based on https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.0
var accessToken = context.HttpContext.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/signalr/calendar")) {
context.Token = accessToken;
}
return;
};
})
.AddOpenIdConnectServer(options => {
options.TokenEndpointPath = "/token";
options.ProviderType = typeof(ApplicationOAuthProvider);
/*...*/
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime) {
app.UseAuthentication();
}
When using the Azure SignalR Service, the OnRetrieveToken event code is simply never hit, which makes sense given that the request is no longer directed at the App Service, but instead to the url of the Azure SignalR Service.
This Hub works while SignalR is hosted on the App Server:
[Authorize(Roles = "Manager, Seller")]
public class CalendarHub : Hub<ICalendarClient> {
private IHttpContextAccessor httpContextAccessor;
public CalendarHub(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor }
public override async Task OnConnectedAsync() {
await Groups.AddToGroupAsync(Context.ConnectionId, GetClaimValue("OrganizationId"));
await base.OnConnectedAsync();
}
private string GetClaimValue(string claimType) {
var identity = (ClaimsIdentity)httpContextAccessor?.HttpContext?.User.Identity;
var claim = identity?.FindFirst(c => c.Type == claimType);
if (claim == null)
throw new InvalidOperationException($"No claim of type {claimType} found.");
return claim.Value;
}
}
But when I switch to the Azure solution:
//From: Startup.cs (abridged)
public IServiceProvider ConfigureServices(IServiceCollection services)
services.AddSignalR().AddAzureSignalR();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime) {
app.UseAzureSignalR(routes => routes.MapHub<CalendarHub>("/signalr/calendar"));
}
...connecting to the hub causes exception No claim of type OrganizationId found. because the identity is completely empty, as if no user was authenticated. This is especially strange, given that I've restricted access to users of specific roles.
Turns out the error is the same as this question where HttpContext is used to get the claim values, because that's what we do everywhere else. And this seems to work as long as it is the App Service itself handling the connection to the client.
But Azure SignalR Service supplies the claims somewhere else:
The correct way is using just Context which has the type HubCallerContext when accessed from a SignalR Hub. All the claims are available from here with no extra work.
So the method for getting the claim becomes
private string GetClaimValue(string claimType) {
var identity = Context.User.Identity;
var claim = identity.FindFirst(c => c.Type == claimType);
if (claim == null)
throw new InvalidOperationException($"No claim of type {claimType} found.");
return claim.Value;
}

Activate Azure Ad authentication when you hit https://host:port/swagger on net Core 2 Api?

I make all changes on my api to use Azure Ad with this and this link features, but when the api is deployed, I need to make the user who gets the Url https://myapi.com/swagger (for example) to redirect it to azure Ad login,then know if the client have rights or not to use this api and redirect it again to my api and show the enpoints he have access.
I make some changes on startup.cs to use OpenIdConnect
//Add AddAzureAdBearer Auth options
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = OpenIdConnectDefaults.AuthenticationScheme;
//options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(option =>
{
option.ClientId = Client;
option.Authority = $"{Instance}/{Tenant}";
option.SignedOutRedirectUri = "https://localhost:44308";
option.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
option.SaveTokens = true;
option.Events = new OpenIdConnectEvents
{
OnRemoteFailure = context =>
{
context.HandleResponse();
return Task.CompletedTask;
}
};
})
.AddCookie()
.AddAzureAdBearer(options => _configuration.Bind("Ad", options));
And I add a HomeController to redirect to swagger UI:
[Authorize]
public class HomeController : Controller
{
[HttpGet("")]
public ActionResult Index()
{
return Redirect("~/swagger");
}
}
When I launch the api, it works as spected, but when y write https://{host:port}/swagger it does not work, don't hit the authentication process and goes to https://{host:port}/swagger/index.html automatically.
How can I fix this?
I'm working with net core 2.0 and Swashbuckle for swagger.
You you need to add Swagger support to ConfigureServices(IServiceCollection services) and to Configure(IApplicationBuilder app, IHostingEnvironment env) in your application’s Startup.cs file. To do so, you need to create a SwaggerServiceExtensions class and add the necessary code to support Swagger in your app.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.Swagger;
namespace JwtSwaggerDemo.Infrastructure
{
public static class SwaggerServiceExtensions
{
public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1.0", new Info { Title = "Main API v1.0", Version = "v1.0" });
c.AddSecurityDefinition("Bearer", new ApiKeyScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = "header",
Type = "apiKey"
});
});
return services;
}
public static IApplicationBuilder UseSwaggerDocumentation(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1.0/swagger.json", "Versioned API v1.0");
c.DocExpansion("none");
});
return app;
}
}
}
Changes in Startup.cs file
Using the above class, the only thing you need to do in your Startup.cs file is the following:
namespace JwtSwaggerDemo
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//... rest of services configuration
services.AddSwaggerDocumentation();
//...
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
//.... rest of app configuration
app.UseSwaggerDocumentation();
}
//.... rest of app configuration
}
}
}
Authorize requests in Swagger UI
Now, when you load the Swagger’s UI address (e.g: https://localhost:44321/swagger/#/), you will see an Authorize button at the top. Clicking on it leads to a modal window, which allows you to authorize your app with a JWT token, by adding Bearer in the value input field.

Getting Windows/Active Directory groups as role claims with Identity Server 4

I have got a basic Identity Server setup as per the UI sample project instructions on GitHub. I have it set it up to use Windows authentication with our on site AD. This is working beautifully.
My issue is with adding the users AD groups to the claims. As per the sample project I have enabled the IncludeWindowsGroups option. Which seems to be adding the claims to the ClaimsIdentity. However, on my MVC client, when I print out the claims I only ever get the same 4. They are sid, sub, idp and name. I have tried adding other claims but I can never get any others to show up.
I have the following as my Client Setup:
return new List<Client>
{
// other clients omitted...
// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
// where to redirect to after login
RedirectUris = { "http://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
RequireConsent = false
}
};
Hopefully I am just missing something simple but I am struggling for ideas now, so any pointers would be much appreciated.
I managed to get this working with a few changes, beyond setting IncludeWindowsGroups = true in the IdentityServer4 project. Note that I downloaded the IdentityServer4 UI quickstart as of the 2.2.0 tag
Per this comment in GitHub, I modified ExternalController.cs in the quickstart UI:
// this allows us to collect any additonal claims or properties
// for the specific prtotocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var roleClaims = claims.Where(c => c.Type == JwtClaimTypes.Role).ToList();
if (roleClaims.Count > 0)
{
additionalLocalClaims.AddRange(roleClaims);
}
I then created a profile service to copy the claims from Windows Auth into the token being sent back:
public class ProfileService : IProfileService
{
private readonly string[] _claimTypesToMap = {"name", "role"};
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
foreach (var claimType in _claimTypesToMap)
{
var claims = context.Subject.Claims.Where(c => c.Type == claimType);
context.IssuedClaims.AddRange(claims);
}
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true; //use some sort of actual validation here!
return Task.CompletedTask;
}
}
and registered with IdentityServer4 in Startup.cs
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(StaticConfig.GetIdentityResources())
.AddInMemoryApiResources(StaticConfig.GetApiResources())
.AddInMemoryClients(StaticConfig.GetClients())
.AddTestUsers(StaticConfig.GetUsers())
.AddProfileService<ProfileService>();
In my client config in IdentityServer4, I set user claims to be included in the Id token. I found that if I tried to map the claims in the callback to UserInfo, that context was lost in IdentityServer4, so the claims wouldn't map.
public static class StaticConfig
{
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
...
AlwaysIncludeUserClaimsInIdToken = true,
...
}
}
}
}
Finally, in Startup.cs for the client website, I did not setup the UserInfo callback; I just made sure that my name and role claims were mapped. Note that if your profile service returns any other claim types, you need to manually map them with a call to a helper method on options.ClaimActions.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.SaveTokens = true;
options.ResponseType = "code id_token";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
//map any other app-specific claims we're getting from IdentityServer
options.ClaimActions.MapUniqueJsonKey("someotherclaimname", "someotherclaimname");
};

Azure App Service Mobile authentication not compatible with MVC/Forms

I have added a Web API service to a 'legacy' MVC project (not vNext), still using Membership and the forms authentication module.
All OK, until I decided to make the web api a mobile service using the latest SDKs for Azure App Services for Mobile.
I have so far narrowed the problem to this
//app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions()
//{
// SigningKey = CloudConfigurationManager.GetSetting("authSigningKey"),
// ValidAudiences = new[] { CloudConfigurationManager.GetSetting("authAudience") },
// ValidIssuers = new[] { CloudConfigurationManager.GetSetting("authIssuer") },
// TokenHandler = GlobalConfiguration.Configuration.GetAppServiceTokenHandler()
//});
This changes the rest of the app so that MVC + forms auth doesn't work. Running out of time to research the problem.
Any clues??
The following solution is working for me:
In your Startup register call UseCookieAuthentication, UseExternalSignInCookie or UseOAuthAuthorizationServer before calling UseAppServiceAuthentication.
Second step:
Add the following class to your project:
private sealed class CustomAppServiceAuthenticationMiddleware : AppServiceAuthenticationMiddleware
{
private readonly ILogger _logger;
public CustomAppServiceAuthenticationMiddleware(OwinMiddleware next, IAppBuilder appBuilder, AppServiceAuthenticationOptions options) : base(next, appBuilder, options)
{
_logger = (ILogger)GetType().BaseType.GetField("logger", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this);
}
protected override AuthenticationHandler<AppServiceAuthenticationOptions> CreateHandler()
{
return new AppServiceAuthenticationHandler(_logger);
}
public override Task Invoke(IOwinContext context)
{
string logLine = $"AppServiceAuthMiddleware: {context.Request.Path}";
if (context.Request.Headers.TryGetValue("Authorization", out var values))
logLine += $"; Authorization: {values.First().Split(' ').FirstOrDefault()}";
if (context.Request.Headers.TryGetValue("X-ZUMO-AUTH", out values))
logLine += $"; X-ZUMO-AUTH: {values.First()}";
_logger.WriteVerbose(logLine);
Debug.WriteLine(logLine);
if (IsZumoAuth(context))
{
return base.Invoke(context);
}
return Next.Invoke(context);
}
private bool IsZumoAuth(IOwinContext context)
{
return context.Request.Headers.ContainsKey("X-ZUMO-AUTH");
}
}
Thrid step:
Replace the app.UseAppServiceAuthentication with the following:
app.Use(typeof(CustomAppServiceAuthenticationMiddleware), app, new AppServiceAuthenticationOptions
{
SigningKey = ...,
ValidAudiences = ...,
ValidIssuers = ...,
TokenHandler = ...
});
This will the owin pipline make call the AppServiceAuthenticationMiddleware just for ZUMO-AUTH auth.
I've got a mixed web & mobile app.
With this approach the membership auth on the web app is working.
In the app, some custom oauth (refresh token based) plus the azure auth (facebook, google, ...) is working too. All that within the same asp.net application.

Resources