I've been following this documentation for using Azure AD B2C for authentication in a blazor web app
https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-azure-active-directory-b2c?view=aspnetcore-5.0
After following this documentation, we're left with a solution containing a server and a client, both running on https port 5001. Now, i'd like to switch to using an external api, rather than the one running on port 5001.
Everything seems good and authentication succeeds when manually using the access token retrieved by blazor. But blazor is only automatically attaching the authentication headers to requests starting with https://localhost:5001.
When i'm instead using https://localhost:5003, the authentication header is left empty.
Is there something i can add to the provider options of my MsalAuthentication, in order for it to pass this access token to my api running on https://localhost:5003?
builder.Services.AddHttpClient("{MyAssembly}.ServerAPI", client => client.BaseAddress = new Uri("https://localhost:5003"))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("{MyAssembly}.ServerAPI"));
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("https://{myproject}.onmicrosoft.com/e3b857b7-df50-4633-ae02-df4d4b20e911/API.Access openid offline_access");
});
If you want to make outgoing requests to URIs that aren't within the app's base URI, you can create a custom AuthorizationMessageHandler class to implement it. For more details, please refer to here
For example
Create Custom AuthorizationMessageHandler class
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "https://localhost:44389/" },
scopes: new[] { "https://<>.onmicrosoft.com/api/user_impersonation" });
}
}
Add the following code in Program.cs.
using System;
using System.Net.Http;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace WebB2C
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
builder.Services.AddHttpClient("ServerAPI", client =>
client.BaseAddress = new Uri("https://localhost:44389/"))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("ServerAPI"));
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("https://<>.onmicrosoft.com/api/user_impersonation");
options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
});
await builder.Build().RunAsync();
}
}
}
Related
I have a blob storage container that stores HTML files. With the public access level, I can see the HTML, but the idea is to set it to private. I want to have users authenticate and access the HTML file.
For that I created an Azure Function with the most relevant part of the code being:
#r "Newtonsoft.Json"
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
public static async Task<IActionResult> Run(HttpRequest req, ILogger log, string version, string route)
{
var blobUri = "https://mybloburi.blob.core.windows.net/" + route;
var expiresOn = req.Headers.FirstOrDefault(p => p.Key.Equals("x-ms-token-aad-expires-on",
StringComparison.OrdinalIgnoreCase)).Value.FirstOrDefault();
log.LogInformation($"expires On : {expiresOn}");
log.LogInformation($"blob uri : {blobUri}");
var isTokenExpired = (DateTime.Parse(expiresOn, styles: DateTimeStyles.AdjustToUniversal) - DateTime.UtcNow).TotalMinutes < 5;
var bearerToken = isTokenExpired? await RefreshToken(req, log) : req.Headers.
FirstOrDefault(p => p.Key.Equals("x-ms-token-aad-access-token", StringComparison.OrdinalIgnoreCase)).
Value.FirstOrDefault();
log.LogInformation($"bearer token: {bearerToken}");
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate");
client.DefaultRequestHeaders.Add("Accept", "*/*");
client.DefaultRequestHeaders.Add("x-ms-version", "2017-11-09");
var response = await client.GetAsync(blobUri);
log.LogInformation($"response: {response}");
var contentType = response.Content.Headers.FirstOrDefault(p => p.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase));
var byteArray = await response.Content.ReadAsByteArrayAsync();
const string defaultContentType = "application/octet-stream";
return new FileContentResult(byteArray, contentType.Value.Any() ? contentType.Value.First() : defaultContentType);
}
}
In the Integration section of the Azure Function, I added this config:
Then I created an App Registration. Under the Authorization option of the Azure Function, I enabled the App Service Authentication. In there I added a new Identity Provider as Microsoft and as a provider I added the created App Registration. I created a Role Group with access to the Blob Storage Container and set its App Id to the "Allowed token audiences".
When I test, I am able to authenticate to the AAD. However, the HTTP Client get function fails with
<Code>AuthenticationFailed</Code>
<Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
I don't know what other configuration to do and I cant seem to find anything in the documentation that could help
As suggested by #Thomas. Yes, we do have a static website where we have a streamlined authentication.
For which you will be having a by default access to a series of pre-configured providers, or you can even register a custom provider.
To configure static web apps for using it as an API function for role assignment you need to add rolesSource property to auth and that is the path of API function.
{
"auth": {
"rolesSource": "/api/GetRoles",
"identityProviders": {
// ...
}
}
}
For more information you can check this document.
I have successfully published Azure App Service from VS-2019 using WebForms. I have successfully secured it so that users must login using an Azure AAD account in the same domain as the App Service. I have successfully created an Azure SQL database. I have successfully added users from the AAD domain to the database and connected to the db, from within the Azure App Service, by hard-coding one of the Azure AAD account users I created, into the connection string.
Now I want to use the authenticated AAD user from the App Service login to connect to the Azure SQL database. Everything I've tried thus far has failed.
I'm pretty new to Azure. Most of my experience is with SQL Server/Visual Studio on an internal corporate domain, with no Cloud services whatsoever.
Anyone have any suggestions?
This is my authentication code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Configuration;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System.Net.Http;
namespace Church
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string aadInstance = EnsureTrailingSlash(ConfigurationManager.AppSettings["ida:AADInstance"]);
private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
string authority = aadInstance + tenantId;
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthenticationFailed = (context) =>
{
return System.Threading.Tasks.Task.FromResult(0);
},
SecurityTokenValidated = (context) =>
{
var claims = context.AuthenticationTicket.Identity.Claims;
var groups = from c in claims
where c.Type == "groups"
select c;
foreach (var group in groups)
{
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Role, group.Value));
}
return Task.FromResult(0);
}
}
}
);
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
private static string EnsureTrailingSlash(string value)
{
if (value == null)
{
value = string.Empty;
}
if (!value.EndsWith("/", StringComparison.Ordinal))
{
return value + "/";
}
return value;
}
}
}
enter code here
This task a is bit more complicated.
In order to achieve what you desire, you have to configure the EasyAuth (the app service Authentication / Authorization service) to also get an access token for Azure SQL DB.
You can read more about access tokens with Azure app service authentication here.
Pay attention to the part for the Azure Active Directory configuration. You will be instructed to go to https://resources.azure.com/, find your app service and update the following properties:
"additionalLoginParams": ["response_type=code id_token",
"resource="]
for the resource parameter, you should use https://database.windows.net/, which is the identifier of Azure SQL DB. This will allow the app service authentication services (EasyAuth) to get an access token, on behalf of the user for Azure SQL DB. You will then be able to get this access token from the HTTP HEADER X-MS-TOKEN-AAD-ACCESS-TOKEN (also described on the same documentation page).
Once you manage to get an access token for Azure SQL DB, then you should use token authentication for azure SQL DB and not user/password based. The token authentication part of the documentation is well hidden here and this example demonstrates it:
string ConnectionString =#"Data Source=n9lxnyuzhv.database.windows.net; Initial Catalog=testdb;"
SqlConnection conn = new SqlConnection(ConnectionString);
conn.AccessToken = "Your JWT token that you took from HTTP HEADER X-MS-TOKEN-AAD-ACESS-TOKEN"
conn.Open();
we got an application deployed as App Service and we are using SignalR for communication. After enabling AAD authentication - in browsers we started receiving 302 responses with redirect location to Azure AD.
Seems like the authentication layer on App Service is ignoring access_token passed by query string.
Request
Request URL: wss://<url>/hubs/chat?access_token=<token>
Request Method: GET
Response
Status Code: 302 Redirect
Location: https://login.windows.net/common/oauth2/authorize?...
After looking everywhere we couldn't find any solution to make this work.
The only solution to this issue that we see is either to disable authentication on App Service or use Long-Pooling, but both options are not acceptable in our situation.
By default, you web application will not get the access token from query string. Commonly, it will get the access token from authorization header or the cookie.
To get the access token from query string, you need to implement your custom authentication way.
Install Microsoft.Owin.Security.ActiveDirectory NuGet package.
Create an authentication provider which will get access token from query string.
public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
var value = context.Request.Query.Get("access_token");
if (!string.IsNullOrEmpty(value))
{
context.Token = value;
}
return Task.FromResult<object>(null);
}
}
Add map in .
app.Map("/yourpath", map =>
{
map.UseWindowsAzureActiveDirectoryBearerAuthentication(new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Provider = new QueryStringOAuthBearerProvider(),
Tenant = tenantId,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = clientId
}
});
map.RunSignalR(hubConfiguration);
});
After multiple calls with Microsoft Technical Support, MS confirmed that App Service Authentication layer doesn't support access token passed in query string and there are no plans for this support yet. So there are two options:
Use different protocol for SignalR (long pooling works just fine)
Drop App Service Authentication
Using a custom middleware, I was able to update the request prior to authorization occurring:
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace Stackoverflow.Example.Security.Middleware
{
public class BearerTokenFromQueryToHeaderMiddleware
{
private readonly RequestDelegate _next;
public BearerTokenFromQueryToHeaderMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var token = context.Request.Query["access_token"];
if (!string.IsNullOrWhiteSpace(token))
{
context.Request.Headers.Add("Authorization", $"Bearer {token}");
}
await _next(context);
}
}
}
I didn't try to get this working with the OpenID framework, but I did test using a custom policy. As long as this is registered earlier than the authentication, then this middleware should execute prior to the framework looking for the token in the header.
I have an Angular Web app with an API that is functioning perfectly (well, as perfectly as any app under development functions) on my local server, but when I migrate it and its associated databases to an App Service on Azure every /api call fails with 500.
So thinking that the problem was with the databases I altered the connection strings on my local development server to point to the Azure databases. I found one small problem this way, I has mispelled the username in the connection string. SO I fixed that and it runs perfectly on my local server while accessing the Azure databases, but as soon as I run it on the Azure App Service using the same connection strings every call to /api fails with Internal Server Error 500.
All regular pages are served perfectly and Angular routing works just fine. Only accessing content from the DB fails. I have been at this for a few days and have no idea what to do next. Any advice welcomed.
I am using OpenIddict for authentication so I tagged that, but I can't see anyway that is relevant. Oddly though, and there is a clue here somewhere, the authentication call to "/connect/token" works and returns a valid token, but "/api/..." URLs do not.
I am using Asp Net Core 2.1 if that is relevant.
More Information
I tried the detailed logs as suggested, but they were hardly detailed. But I did note one interesting item. In the error there was the following information:
Requested URL: https://mysite.azuurewebsites.net/api/accounts/getusers
Physical Path: D:\home\site\wwwroot\api\accounts\getusers
Now this app is using MVC so there is no such Physical Path. The Controller is decorated with:
[Route("api/accounts")]
and the Action is decorated as:
[Authorize(Roles = "Somerole")]
[HttpGet("GetUsers"), Produces("application/json")]
It seems to me the route mapping is failing. But this works beautifully on my local development computer. What could be different on the Azure App Service? Is there some special setting I need to set in the portal to allow MVC? I can't imagine why the portal should care about such matters.
Even More Information
Using Postman, if I access /api/someValidUrl with a valid Bearer token I get a 500 error. If I remove the Authorization header then I get a 401 returned.
I started off by saying I didn't think it had anything to do with OpenIddict, but maybe I was wrong. My Authorization Controller simply creates the token. All the checking for validity is done by OpenIddict.
A Huge Clue
I added an ExceptionHandler and then used Postman to make an API request and that yielded the following exception:
<h1>Error: IDX20803: Unable to obtain configuration from: '[PII is hidden by default. Set the 'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]'.</h1>
at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
I found an explanation here but I don't fully understand this but it looks as though MS middleware on Azure is incorrectly trying to interpret it as an Azure AD request. The only thing I know for sure is I do not have a file called IdentityModelEventSource.cs in my project.
For reference https://mywebsite.azurewebsites.net/.well-known/openid-configuration returns:
{
"issuer": "https://mywebsite.azurewebsites.net/",
"token_endpoint": "https://mywebsite.azurewebsites.net/connect/token",
"jwks_uri": "https://mywebsite.azurewebsites.net/.well-known/jwks",
"grant_types_supported": [
"password"
],
"scopes_supported": [
"openid",
"email",
"profile",
"roles"
],
"claims_supported": [
"aud",
"exp",
"iat",
"iss",
"jti",
"sub"
],
"subject_types_supported": [
"public"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"claims_parameter_supported": false,
"request_parameter_supported": false,
"request_uri_parameter_supported": false
}
Perhaps with this information someone can point me in the right direction.
New Startup.cs
I took Pinpoint's advice and changed from JWT. The new Startup follows:
using AspNet.Security.OpenIdConnect.Primitives;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SIAngular.DBContexts;
using SIAngular.Models;
using SIAngular.Services;
using OpenIddict.Abstractions;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using System;
using Microsoft.Extensions.Logging;
namespace SIAngular
{
public class Startup
{
private readonly IHostingEnvironment env;
public Startup(IHostingEnvironment env, IConfiguration configuration)
{
Configuration = configuration;
this.env = env;
SIDBConnectionString = Configuration.GetConnectionString("SIDB");
}
public static string SIDBConnectionString;
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
SymmetricSecurityKey _ssk = new SymmetricSecurityKey(Convert.FromBase64String(Configuration["Jwt:Key"]));
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("SqlConnection"));
options.UseOpenIddict();
});
services.AddCors();
// Register the Identity services.
services.AddIdentityCore<ApplicationUser>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.Password.RequireDigit = true;
config.Password.RequiredLength = 8;
config.Password.RequireLowercase = true; config.Password.RequireNonAlphanumeric = true;
config.User.RequireUniqueEmail = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddRoleValidator<RoleValidator<IdentityRole>>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<ApplicationUser>>();
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
services.AddOpenIddict()
// Register the OpenIddict core services.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})
// Register the OpenIddict server services.
.AddServer(options =>
{
// Register the ASP.NET Core MVC services used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.UseMvc();
// Enable the token endpoint.
options.EnableTokenEndpoint("/connect/token");
options.AcceptAnonymousClients();
options.DisableScopeValidation();
// Note: the Mvc.Client sample only uses the code flow and the password flow, but you
// can enable the other flows if you need to support implicit or client credentials.
options.AllowPasswordFlow();
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles);
// During development, you can disable the HTTPS requirement.
if (env.IsDevelopment())
options.DisableHttpsRequirement();
options.AddSigningKey(_ssk);
})
.AddValidation();
services.AddSingleton<IConfiguration>(Configuration);
services.AddScoped<IPasswordHasher<ApplicationUser>, SqlPasswordHasher>();
services.AddMvc();
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCors(builder =>
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
//app.UseWebpackDevMiddleware(new Microsoft.AspNetCore.SpaServices.Webpack.WebpackDevMiddlewareOptions { HotModuleReplacement = true });
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.MapWhen(x => !x.Request.Path.Value.StartsWith("/api"), builder =>
{
builder.UseMvc(routes =>
{
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
});
}
}
}
Now the problem is an exception:
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
We would like to build an Admin Checklist page that allows our network administrators to quickly view all the setting in the IIS and web config to easily trouble an issue. the web config is fairly easy but I'm not sure how to get the stuff from IIS. (App Pool Name and type, Machine Key, Anonymous Authentication, etc.) I'm sure it can be done, I just don't know how.
Thanks,
Rhonda
The information you're looking for are exposed in the IIS .Net APIs. You can find them in \windows\system32\inetsrv (they are Microsoft.Web.Administration.dll, and maybe Microsoft.Web.Management.dll).
Sample code to get ID and application pool name for each site :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Web.Administration;
namespace ConsoleApplication79
{
class Program
{
static void Main(string[] args)
{
ServerManager s = new ServerManager();
var q = s.Sites.Select(aSite => new
{
ID = aSite.Id,
AppPoolName = aSite.Applications.First().ApplicationPoolName
});
foreach (var item in q)
{
Console.WriteLine("ID: {0}, PoolName: {1}", item.ID, item.AppPoolName);
}
}
}
}