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

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!

Related

MS Identity Azure app registered but sends unauthorized_client in implicit flow

I have registered an app in Azure for Microsoft Identity platform. I configured it to allow MS Accounts (e.g. outlook.com) and have basically done everything in a few of the quickstarts online here and here (except for "add credentials to your web app"). I have also checked the boxes that enable implicit flow.
I redirect my React application to the URL to sign in (using implicit flow), I get to enter my username but then I see
unauthorized_client: The client does not exist or is not enabled for consumers. If you are the application developer, configure a new application through the App Registrations in the Azure Portal at https://go.microsoft.com/fwlink/?linkid=2083908
Like I mentioned above, I've gone through several quick starts and read about implicit flow here and followed their examples for my code.
I also tried just deleting the app registration and starting over. No luck.
JS Code attempting to implement Implicit Flow
JS code that redirects the browser to a Url that looks like Microsoft's first example on their implicit flow page
goSignIn() {
const tenant = 'common'; // (for us with MS accounts)
const clientId = '*****';
const redir = encodeURIComponent('http://localhost:3000/signin');
const nonce = Math.round(Math.random() * 10000, 0);
const uriTemplate = 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?client_id={clientId}&response_type=id_token+token&redirect_uri={redirect}&scope=openid&response_mode=fragment&state={state}&nonce={nonce}';
const filledTemplate = uriTemplate
.replace('{tenant}', tenant)
.replace('{clientId', clientId)
.replace('{redirect}', redir)
.replace('{nonce}', nonce)
.replace('{state}', nonce);
console.log(filledTemplate);
window.location = filledTemplate;
}
App Configuration in Azure:
Azure -> Identity -> App Registrations -> MyApp -> Authentication
Redirect Uri: http://localhost:3000/signin (React app runs on 3000 and I have a route configured for /signin)
Not using any suggested Redirects.
Checked Implicit checkboxes for ID Token and Access Token
Live SDK support enabled
Supported account types is set to "Accounts in any organizational directory and personal Microsoft accounts (e.g. Skype, Xbox, Outlook.com)"
Azure -> Identity -> App Registrations -> MyApp -> API Permissions
MS Graph
User.Read
Email
Profile
openid
From the docs I read, I thought I had done enough to the id token. I'm not sure what tweak must be made in order to get it to work.
I experienced an issue like this one. The mistake I made has to do with the App ID: when you create the client secret the Azure UI will present the secret and the secret ID. This secret ID is not the one to use in your app's configuration. Rather, you need the Application ID found on the Overview page.
I imagine that there are many configuration problems which can produce this error message. In general: pay close attention to the App ID, if the error is that the app is not found.
It seems that you have done enough to get the token. I have tested this on my side, it works well. Here I provide you with my screenshot for you to check again.
Also, here is my working request url, you can login with your msa to have a test.
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
client_id=5fd66168-7ba3-4bbc-a155-bff662eed9f7
&response_type=id_token+token
&redirect_uri=http://localhost:3000/signin
&scope=openid
&response_mode=fragment
&state=12345
&nonce=678910

AADSTS50011: The reply url specified in the request does not match the reply urls configured for the application: '

I have created a API app and deployed it to Azure. The app uses Active directory authentication.
I get the following error
AADSTS50011: The reply url specified in the request does not match the reply urls configured for the application: 00000000-0000-4f27-0000-00000000.
Steps so far
Enabled Active directory authentication
Set CORS to *
Set Reply URL to same address https://myapp.azurewebsites.net/
I have added the following settings in the web config
config
<add key="ida:AADInstance" value="https://login.microsoftonline.com/{0}"></add>
<add key="ida:PostLogoutRedirectUri" value="https://myapp.azurewebsites.net/"></add>
The code for the api is as follows
[HttpGet]
[SwaggerResponse(HttpStatusCode.OK,
Type = typeof(IEnumerable<Contact>))]
public async Task<IEnumerable<Contact>> Get()
{
return await GetContacts();
}
Get a Fiddler trace of what goes through your browser when you try to authenticate to the app. There should be a request to AAD asking for authentication, which will also include a reply url. Make sure it is the same as the one your app is configured with in AAD.
Have you set below key and value on web.config.
key="ida:RedirectUri" value="https://myapp.azurewebsites.net/"
Had the same error, the solution was:
Go to the Azure portal: https://portal.azure.com sign in and click on the Azure Active Directory icon on the left. Then click on the ‘App registrations’ icon in the middle pane. In the search box enter the application from the error message and choose ‘All apps’ from the dropdown:
Click on your application, then the Settings icon, select the ‘Reply URLs’ from the list.
Copy One of reply URL and add it in your application as an https port.
You can do that from properties of the project or just add in lounchsetting.json files sslPort value
On the AAD App Registration, append the AAD auth callback to your Reply URI value. Originally I had mine set to something like:
https://my-app.azurewebsites.net
I then updated this to:
https://my-app.azurewebsites.net/.auth/login/aad/callback
After that, the error was cleared & I was able to call my API endpoints.
I had this same issue when following the MS tutorial Call the Microsoft Graph API from a Windows Desktop app. There was no place in my code where I was supplying a redirect url, except on this line
.WithDefaultRedirectUri();
Which upon hovering over it I could see was https://login.microsoftonline.com/common/oauth2/nativeclient, which was a redirect uri that was present on my app on Azure. This was all quite confusing and I did not find an answer anywhere online. After about 3 hours of searching for answers and playing around with many possibilities in the code I found this comment in one of the files I downloaded:
// Requires redirect URI "ms-appx-web://microsoft.aad.brokerplugin/{client_id}" in app registration
So I went to https://ms.portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Authentication/appId/171axxxx-xxxx-xxxx-xxxx-xxxxxxxxafff/isMSAApp/ and under Authentication >Redirect URIs I added the redirect URI ms-appx-web://microsoft.aad.brokerplugin/171axxxx-xxxx-xxxx-xxxx-xxxxxxxxafff.
Next I modified my code like this:
public partial class App : Application
{
// Below are the clientId (Application Id) of your app registration and the tenant information.
// You have to replace:
// - the content of ClientID with the Application Id for your app registration
// - The content of Tenant by the information about the accounts allowed to sign-in in your application:
// - For Work or School account in your org, use your tenant ID, or domain
// - for any Work or School accounts, use organizations
// - for any Work or School accounts, or Microsoft personal account, use common
// - for Microsoft Personal account, use consumers
private static string ClientId = "171axxxx-xxxx-xxxx-xxxx-xxxxxxxxafff";
// Note: Tenant is important for the quickstart.
private static string Tenant = "72f9xxxx-xxxx-xxxx-xxxx-xxxxxxxxdb47"; //also works with "common"
private static string Instance = "https://login.microsoftonline.com/";
private static IPublicClientApplication _clientApp;
static App()
{
CreateApplication(true);
}
public static void CreateApplication(bool useWam)
{
//initialize MSAL
var builder = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority($"{Instance}{Tenant}")
.WithDefaultRedirectUri();
if (useWam)
{
builder.WithExperimentalFeatures();
builder.WithBroker(true); // Requires redirect URI "ms-appx-web://microsoft.aad.brokerplugin/{client_id}" in app registration
}
_clientApp = builder.Build();
TokenCacheHelper.EnableSerialization(_clientApp.UserTokenCache);
}
public static IPublicClientApplication PublicClientApp { get { return _clientApp; } }
}
I don't know why Azure didn't just add this in with the other redirects since they knew it would be needed, or at least include this step in the instructions, but hopefully this will be helpful to someone else experiencing this error.

ServiceStack Metadata Redirect behind a Azure App Gateway not working

My api is hosted on Azure as an App Service with an Azure App Gateway in front of that.
I have set the webhosturl in my startup and that is working as when I view the metadata page, i see the links pointing to the correct location. And those links work. However when I navigate to the base url for my api, it redirects me to the app service url.
Here is a snippet of my startup...
SetConfig(new HostConfig
{
WebHostUrl = "https://api-dev.hsawaknow.net/link/",
DefaultRedirectPath = "/metadata",
DebugMode = AppSettings.Get(nameof(HostConfig.DebugMode), false)
});
Please see the links below and see the differences.
https://api-dev.hsawaknow.net/link/metadata
vs
https://api-dev.hsawaknow.net/link/
You will get an https error as I am using a self signed cert, until I get this figured out. I have seen other posts that say to make this change and that it works, but not for me.
Please help!
I have this figured out. There were a couple things that I had to do.
First thing I had to do was setup the forwarded headers middleware to recognize and process the correct headers coming from the Azure Application Gateway.
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHostHeaderName = "X-ORIGINAL-HOST";
options.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor;
});
This allowed my site to work with links to the correct pages without setting the WebHostUrl. The only caveat about using the Azure App Gateway is that it uses X-ORIGINAL-HOST instead of the standard X-FORWARDED-HOST.
Next, I had to set the DefaultRedirectPath on the HostConfig dynamically based on settings in appsettings.json. In the case of the Azure App Gateway, my public url was https://api-dev.hsawaknow.net/link/, I had to set the redirect to /link/metadata, instead of just metadata, because of how the host header was getting set in the previous step.
It took a few tries, but this configuration works well, when hosting on Azure App Services fronted with an Azure Application Gateway.
Kudos to the mythz for the quick response, which pointed me in the right direction.
Enable the Request Logger so you can see what requests ServiceStack receives.
Does it work when not specifying a WebHostUrl?

Unable to get App Service CDN to work with authentication

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.

How can I secure simple HTML files using Azure AD?

I have a legacy static website that is just plain HTML and simple JavaScript for UI effects. There is no server side code, api, config files or anything in this website - just raw HTML files, CSS, pictures, etc.
The website will not be hosted in Azure. It will be on a local IIS server. If I pull the web site into Visual Studio, the "Configure Azure AD Authentication" wizard shows:
An incompatible authentication configuration was found in this project
().
How can I secure simple HTML files using Azure AD?
The Visual Studio "Configure Azure AD Authentication" wizard is intended for ASP.Net Web Apps and Web APIs.
In your case, what you are building is considered a "Single Page Application" or SPA. Even though you might have multiple pages, this term also applies to client side only web apps with no backend code.
For this, you should follow the Azure AD Javascript Single Page Application sample.
The gist of it is that you should us ADAL.js like shown in this sample's app.js, along the lines of:
// Configure ADAL
window.config = {
instance: 'https://login.microsoftonline.com/',
tenant: '[Enter your tenant here, e.g. contoso.onmicrosoft.com]',
clientId: '[Enter your client_id here, e.g. g075edef-0efa-453b-997b-de1337c29185]',
postLogoutRedirectUri: window.location.origin,
cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
};
var authContext = new AuthenticationContext(config);
// Check For & Handle Redirect From AAD After Login
var isCallback = authContext.isCallback(window.location.hash);
authContext.handleWindowCallback();
$errorMessage.html(authContext.getLoginError());
if (isCallback && !authContext.getLoginError()) {
window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
}
// Check Login Status, Update UI
var user = authContext.getCachedUser();
if (user) {
//Do UI for authenticated user
} else {
//Show UI for unauthenticated user
}
// Register NavBar Click Handlers
$signOutButton.click(function () {
authContext.logOut();
});
$signInButton.click(function () {
authContext.login();
});
Note: There's also a Angular SPA sample.
The solution posted by Saca pointed me in the right direction, but adding the JS to every page was not a valid solution for me. There were thousands of HTML files, lots with no common JS file I could tack that ADAL code into. I would have had to find a way to insert that JS on all those pages.
My first solution was simply creating a normal .NET MVC app with the proper auth configured. Then I simply loaded this legacy content via an iFrame. This worked but was limiting for the users.
As Fei Xue mentioned in another comment, the next solution involved scrapping the iFrame but routing all requests for static files through a controller. Using this as a reference for understanding that: https://weblogs.asp.net/jongalloway/asp-net-mvc-routing-intercepting-file-requests-like-index-html-and-what-it-teaches-about-how-routing-works
The above solutions worked. However, eventually this app ended up as an Azure App Service and I simply turned on authentication at the app service level with just the pure html files.

Resources