Unable to get App Service CDN to work with authentication - azure

I have an asp.net core web application hosted in azure as an app service. I've configured the application to use OpenId Connect with Azure AD as the authority. The authentication happens within my application (I am not configuring the app service itself to handle the authentication).
Everything works fine when I hit the app service directly (or even if I use a custom domain name).
I've enabled the CDN service within the app service. Once the initial propagation finished, I open a browser and navigate to the CDN address ([name].azureedge.net).
I get a redirect to Azure AD, but once I finish the authentication process, I get an error.
It looks like when the redirect to Azure AD came back from the CDN, the app service's URL was set as the return_url. So when Azure AD redirected me, I was no longer hitting the CDN. When the redirect to Azure AD happens, there's a cookie placed in my browser; I suspect my site is looking for that cookie, but the browser didn't send it because it was set by a different domain.
I've tried configuring the CallbackPath in the OpenIdConnectOptions to the full URL (schema, host, domain, etc), but when my application initializes, and error is thrown saying that the path must start with a '/' (presumably it's expecting a path from the root of the domain in the request).
Hopefully someone else has come across this problem and can tell me what I'm doing wrong.
Per request, here's my OIDC configuration:
var openIdOptions = new OpenIdConnectOptions
{
ClientId = adSettings.ClientId,
ClientSecret = adSettings.ClientSecret,
Authority = adSettings.Authority,
CallbackPath = adSettings.CallbackPath,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Events = new OpenIdConnectEvents { OnTicketReceived = AddApplicationRolesToUserClaimsAsync, OnAuthorizationCodeReceived = RedeemCodeAsync }
};
foreach (var scope in adSettings.Scopes.Concat(settings.MicrosoftGraph.Scopes))
openIdOptions.Scope.Add(scope);
application.UseOpenIdConnectAuthentication(openIdOptions);
adSettings is a POCO that is hydrated from the following appsettings.json:
"AzureAd": {
"AADInstance": "https://login.microsoftonline.com/",
"ClientSecret": "REDACTED",
"CallbackPath": "/signin-oidc",
"ClientId": "REDACTED",
"TenantId": "REDACTED",
"Scopes": [
"openid",
"profile",
"offline_access"
]
}
adSettings.Authority is defined in the POCO as:
public string Authority => $"{AADInstance}{TenantId}/v2.0";

After digging around a bit, I found the answer.
The OpenIdConnectOptions.Events property allows you to hook into various events that happen throughout the lifecycle of authentication. One callback is called OnRedirectToIdentityProvider. It provides a RedirectContext. On that object you can read/write to a property called ProtocolMessage.RedirectUri. That property allows you to specify a full URL which is used as the return_url when the user is forwarded to AAD.
It's worth noting that I'm using the Microsoft.AspNetCore.Authorization.OpenIdConnect packge from Nuget. There are other packages available that provide similar functionality that do allow you to set a full URL in the options object.

Related

Problem calling Microsoft Graph from ASP.NET Grpc service

I have two applications -
public client application (.NET Core console app), in which user gets Microsoft identity access token
web API, which tries to call Microsoft Graph on-behalf-of user, using that access token
When I call Microsoft Graph from web API, I get a MicrosoftIdentityWebChallengeUserException, which inner exception states:
"The user or administrator has not consented to use the application with ID <...> named <...>. Send an interactive authorization request for this user and resource."
I've tried:
to pre-authorize client application in service application using Expose an API tab in Azure Portal
to add client application ID in knownClientApplications array in Manifest tab
to include the scopes, needed for Microsoft Graph (e.g. "User.Read"), in the access token, obtained by the user
but it seems that this does not work and I still get the same exception.
The question is - can I somehow avoid this exceptional situation by getting all needed permissions in a user access token, before calling the GRPC service, or if not, that how do I need to handle this exception to propagate it back to the user.
Full details here. Keep following the Next Steps.
Basically, you'll need to:
Include the Microsoft.Identity.Web and Microsoft.Identity.Web.MicrosoftGraph NuGet packages in your API project.
Set up a Client Secret or a Certificate in the Azure App Registration. Include that in your appsettings.json file:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "{YOUR-DOMAIN-NAME-FROM-APP-REGISTRATION}",
"TenantId": "{YOUR-TENANT-ID-FROM-APP-REGISTRATION}",
"ClientId": "{YOUR-CLIENT-ID-FROM-APP-REGISTRATION}",
"Scopes": "{YOUR-API-ACCESS-SCOPE-FROM-APP-REGISTRATION}",
"CallbackPath": "/signin-oidc",
"ClientSecret": "{YOUR-CLIENT-SECRET-FROM-APP-REGISTRATION}"
}
Include the following section in your appsettings.json file:
"Graph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "User.Read"
}
Include the following code in your Project.cs file or Startup.cs file (depending on what version of .Net you're using):
Startup.cs:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration, Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(Configuration.GetSection("Graph"))
.AddInMemoryTokenCaches();
Project.cs:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
.AddInMemoryTokenCaches();
From there, you just need to inject the GraphServiceClient into your controller or page constructor. The link above provides code for implementation in an ASP.NET API. I'm using this method in a Blazor Webassembly hosted app, so my implementation needs varied slightly from the instructions, but it's running/working as it should.

Configuring an Azure Static Web App to authenticate using Azure AD B2C

I've built a basic Vue web app using Azure Static Web Apps, and I'm trying to configure custom authentication. I've already managed to get everything (mostly) working using Auth0 by following the documentation and referencing this handy blog post.
For Auth0, I added AUTH0_ID=<my-auth0-id> and AUTH0_SECRET=<my-auth0-secret> to the local.settings.json file. My staticwebapp.config.json looked like this:
...
"auth": {
"identityProviders": {
"customOpenIdConnectProviders": {
"auth0": {
"registration": {
"clientIdSettingName": "AUTH0_ID",
"clientCredential": {
"clientSecretSettingName": "AUTH0_SECRET"
},
"openIdConnectConfiguration": {
"wellKnownOpenIdConfiguration": "https://<my-auth0-tenant>/.well-known/openid-configuration"
}
},
"login": {
"nameClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"scopes": ["openid", "profile"]
}
}
}
}
}
I'm now trying to set up authentication using Azure AD B2C. My understanding is that Azure Static Web Apps handles a portion of the authentication such that I should configure the ID provider to work with a web app rather than with a single page app framework. This is what I did when using Auth0 and it seemed to work.
I've added AADB2C_ID=<my-azure-ad-b2c-id> and AADB2C_SECRET=<my-azure-ad-b2c-secret> to the local.settings.json file. In staticwebapp.config.json I replaced ClientIdSettingName to AADB2C_ID, clientSecretSettingName to AADB2C_SECRET, and wellKnownOpenIdConfiguration to https://<my-azure-ad-b2>.b2clogin.com/<my-azure-ad-b2c>.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_signupsignin1. This references the 'signupsignin' user flow on my B2C tenant.
At this point I can visit /login which points to /.auth/login/aadb2c, initiates the user flow, and lets me sign up and verify as expected. The test user is then created in my Azure AD B2C tenant. However, B2C then tries to redirect me to /.auth/complete which throws a 403 error:
We need an email address or a handle from your login service. To use
this login, please update your account with the missing info.
I've tried adding /.auth/complete as an allowed redirect URI in Azure AD B2C but this doesn't fix things. What am I missing here?
Try changing to this: "nameClaimType": "emails".
Sourced from staticwebapp.config.json in here: https://github.com/Azure/static-web-apps/issues/457

Best practices for using Azure CDN in scenarios that require authentication to access?

I've never configured a CDN before and I'm looking for guidance for how to approach the configuration for my scenario.
For context, I'm currently using Azure Verizon Premium CDN and an Azure App service (.Net Core). My authentication platform is Azure Active Directory.
I have a static web app that has lots of static content (hundreds of files). Users hit the website at (www.mysite.azurewebsites.net/index.html). Index.html then fetches large amounts of static content (various, images, video, metadata). Initially, the requirement was to just serve the content publicly but now I have requirements around restricting access to some the content based on whether or not a user has a certain role & the user hits a certain path. I also need to be able to keep some content public and highly available to end users.
Before authorization requirements came in, I was able to successfully set up a CDN endpoint (www.mysite-cdn.azureedge.net) and point it to my app service no problem! I can hit the site and see the site content from the cdn endpoint without auth no issue.
Problems came when I added authentication to my site my CDN is redirected during the authentication flow back to the origin endpoint. The origin authentication middleware (Nuget: Microsoft.Identity.Web) automatically assembles the callback uri for AAD as "www.mysite.azurewebsites.net/signin-oidc". So the present flow for a user hitting the CDN endpoint returns them to an error page on the app service endpoint. Although if they manually redirect to "www.mysite.azurewebsites.net" they are logged in, I don't want to redirect the user back to origin, I want to keep them on www.mysite-cdn.azurewebsites.net.
Essentially, I want to be able to enable these flows:
Public End User -> CDN Endpoint + Public path -> (CDN request origin and caches) -> End user sees site at CDN endpoint
Internal End User -> CDN Endpoint + Private path -> (CDN request origin and has access) -> User is permitted into site at CDN endpoint
Internal End User -> CDN Endpoint + Private path -> (CDN request origin and DOESN’T have access) -> User is redirected to public CDN endpoint (or unauthorized page)
This is the auth check for each static file in OnPrepareResponse for static file options. This checks authentication before requesting a static asset in this folder on the server. This works fine without the CDN. It should be noted that I also do role checks and this has been simplified for the sake of the example as it repos with Authentication.
OnPrepareResponse = staticContext =>
{
// require authentication
if (authenticate &&
!staticContext.Context.User.Identity.IsAuthenticated)
{
// Redirect back to index sign in page all unauthorized requests
staticContext.Context.Response.Redirect(unauthenticatedRedirectPath);
}
},
I did find this stack overflow which seemed similar to my problem however I am using a different NuGet package (Microsoft.Identity.Web). I implemented a check to redirect however that did not seem to work and cause an infinite loop when trying to login.
Action<MicrosoftIdentityOptions> _configureMsOptions = (options) =>
{
options.Instance = Configuration["AzureAd:Instance"];
options.Domain = Configuration["AzureAd:Domain"];
options.ClientId = Configuration["AzureAd:ClientId"];
options.TenantId = Configuration["AzureAd:TenantId"];
options.CallbackPath = Configuration["AzureAd:CallbackPath"];
options.Events.OnRedirectToIdentityProvider = async (context) =>
{
// This check doesn’t work because the request host always is mysite.azurewebsites.net in this context
// if (context.Request.Host.Value.Contains("mysite-cdn"))
// {
context.ProtocolMessage.RedirectUri = "https://" + "mysite-cdn-dev.azureedge.net/" + Configuration["AzureAd:CallbackPath"];
//}
};
};
I've started looking into Azure Front door, as that seems to be more applicable to my use case but haven't set it up. It provides some level of caching/POP as well as security. It looks like it's also possible to use with Azure AD with some web server tweaking. It would be good to know from others if Azure Front Door sounds like a more sensible CDN solution approach vs Azure CDN.
I've also looked into Azure CDN Token authentication- but that seems to be something that also requires me to stamp each request with an Auth token. It changes my architecture such that I can no longer just 'point' my cdn at the web app and instead my app would give the browser a token so it could securely request each asset.
All that said, I'm looking for guidance on how best to configure an application using a CDN while also using authentication. Thanks!

Allowing public access to only 1 endpoint of Azure Application

I'm building a small Azure Serverless Application with 3 distinct functions triggered by 3 different HTTP events. I set the "Authorization Level" to "Anonymous" for each of the functions. Then I set up the Authentication (on the application level) to link to my Azure Active Directory. Although it took me some time to figure out that part, in the end it appears to work like a charm. The issue is that right now I'm required to pass the bearer token for each and every one of these functions, whereas I need one of them to be publicly accessible.
Is there any way to do that that does not require me to split that one function into a separate Azure Application?
I think URL authorization rules should help here.
For your scenario, Enable the Authentication/Authorization and allow anonymous access in the portal. Next, you'll want to create an authorization.json file in the root of your site and define two routes:
Disable anonymous access at the root.
Enable anonymous access for the anonymous function URL.
authorization.json
{
"routes": [{
"path_prefix": "/",
"policies": { "unauthenticated_action": "RedirectToLoginPage" }
},{
"path_prefix": "/api/HttpTrigger1",
"policies": { "unauthenticated_action": "AllowAnonymous" }
}]
}
NOTE: Make sure to Stop/Start Function App after enabling the Authentication/Authorization and adding the authorization.json file at wwwroot folder.
Not sure how you implemented the app level security but in a regular OWIN or CORE app, the token handler will just pass the request as unauthenticated to the controller method if it does not find a valid token in the Authorization header (no header or token invalid). It's up to the method then to decide whether to handle the request or reject as unauthorized. Can you change your token verifier accordingly? (In fact, if your API can receive tokens issued by different issuers - AAD, B2C, Ping - you can configure several token handlers, each will try to va

Getting an infinite redirect loop on Azure App Service with ADB2C and a shared Auth Cookie

tl;dnr
In essence, my app was working fine until implementing a shared cookie. Now I have an infinite login redirect loop. I've spent hours mucking about with cookie configurations, testing locally, deploying to Azure, and failing. How can I force HTTPS inside an Azure App Service?
Like many others, I've run into the infinite login loop issue. My application was working fine with ASP.net Core 2.2 (on Dot Net 4.7.1) and then I moved to a "shared Auth cookie". In development, everything works fine from localhost, but as soon as I publish to an Azure App service (.azurewebsites.net domain) , I get the infinite login redirect loop.
To start things off, using an Azure Key Vault, I set up the "Data Protection" services following these instructions link.
and set my shared cookie as such for all applications:
services.AddDataProtection()
.SetApplicationName("custom-app")
.PersistKeysToAzureBlobStorage(cloudStorageAccount, azureBlobKeyStoragePath)
.ProtectKeysWithAzureKeyVault(keyVault, clientId, clientSecret);
var authCookie = new CookieBuilder() {
Name = ".AspNetCore.Auth.SharedCookie",
Path = "/",
Domain = ".mycustomdomain.com",
SecurePolicy = CookieSecurePolicy.SameAsRequest, // tried None as well without luck
SameSite = SameSiteMode.None, // I've tried Lax without any changes
HttpOnly = false,
IsEssential = true //changing this makes no difference
};
services
.AddAuthentication(sharedOptions => {
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultAuthenticateScheme = OpenIdConnectDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAdB2C(options => configuration.Bind("AzureAdB2C", options))
.AddCookie(options => { options.Cookie = authCookie; });
...
var corsOrigins = new[] {
"http://localhost",
"https://localhost",
"http://*.mycustomdomain.com",
"https://*.mycustomdomain.com",
"http://*.azurewebsites.net",
"https://*.azurewebsites.net",
"https://*.onmicrosoft.com",
"https://login.microsoftonline.com"
};
app.UseCors(cors => {
cors.WithOrigins(corsOrigins)
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowCredentials();
cors.AllowAnyHeader();
cors.AllowAnyMethod();
});
Other than configuring the "Data Protection" services and additional CORS domains, no other changes were made to the application code.
My application is configured for HTTPS ... https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-2.2
...
app.UseHsts();
...
app.UseHttpsRedirection();
...
And I also verified that my Azure App Service is set for HTTPS access on both the "Custom domains" and "SSL settings" options.
Based on all the posts I have come across, I understand that the main issue may have to do with HTTPS redirecting from the Azure ADB2C endpoints and the shared cookie not being stored/read properly. THe reverse proxy drops the HTTPS and only calls HTTP. I just can't seem to figure out which combination should work. I am using the OutofProcess host, and I can see that the internal calls are HTTP. How can I get these to be HTTPS?
Side Note: I tried changing the Correlation or Nonce cookie domains as well, but this results in Correlation Errors.
TIA, any direction pointing would be appreciated.
Short list of posts I've referenced:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/azure-ad-b2c?view=aspnetcore-2.2
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-2.2
How can I share Cookie Authentication across apps in Azure with .Net Core?
Share Cookie authentication between ASP.NET Core 2.2 and ASP. NET MVC 5 (.NET Framework 4.6.1) without Microsoft.Identity
Azure AD login inifinite loop
AspNet Core Identity - cookie not getting set in production
.net core 2.0 cookie authentication getting stuck in infinite redirect loop when trying to access over https
ASP.NET Core 2.1 cookie authentication appears to have server affinity
Configure cors to allow all subdomains using ASP.NET Core (Asp.net 5, MVC6, VNext)
https://github.com/aspnet/Security/issues/219
https://github.com/aspnet/Security/issues/1231
https://github.com/aspnet/Security/issues/1583
https://blogs.msdn.microsoft.com/benjaminperkins/2017/11/30/how-to-make-an-azure-app-service-https-only/
After some time off studying my configuration and creating a test app from scratch ... I came across the following post:
ASP.NET Core Sharing Identity Cookie across azure web apps on default domain (*.azurewebsites.net)
My current assumption is that this is indeed the culprit, I will try a new test tonight with one of my custom domains and see what happens.

Resources