Azure AD Redirect URL Using Application Gateway - asp.net-core

We have an ASP Core 2.0 App working nicely with Azure AD on the private network. However, we've been playing around with the Azure Application Gateway, investigating the possibility of allowing access to the app from outside for remote workers etc.
We have registered the app on the Gateway, and, once logged in with Azure AD, the anonymous front page is accessible via ourapp.msappproxy.net. However, when signing in (again?) in the app, the client is redirected back to intervalServer/signin-oidc which fails as it is not accessible externally.
While I doubt this is any part of the solution, I have tried overriding the redirect "CallbackPath": "/signin-oidc", to absolute path ourapp.msappproxy.net/signin-oidc but I can't seem to work out how. Changing the reply URL in Azure Portal doesn't help either (although I doubted it would, this is just for verification right?).
I can't seem to find any guidance on this on this particular scenario, so that would be welcome. Otherwise, I'm left pondering the following:
1, If I could change the redirect to ourapp.msappproxy.net/signin-oidc, would that solve the sign in issue?
2, Do I even need an additional sign in step, or should I be changing the app to accept AzureAppProxyUserSessionCookie or AzureAppProxyAccessCookie? (If that's even an option?)

Thanks to rfcdejong in the comments for putting me on track. In our case I was able use Azure AD with the Azure Application Gateway by overriding OnRedirectToIdentityProvider event and supplying the proxy url in ConfigureServices
services.AddAuthentication(...)
.AddOpenIdConnect(options =>
{
options.ClientId = Configuration["Authentication:AzureAD:ClientId"];
options.Authority = Configuration["Authentication:AzureAd:Authority"];
options.CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"];
if (IsProduction) // So that I can use the original redirect to localhost in development
{
Task RedirectToIdentityProvider(RedirectContext ctx)
{
ctx.ProtocolMessage.RedirectUri = "https://ourapp.msappproxy.net/signin-oidc";
return Task.FromResult(0);
}
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = RedirectToIdentityProvider
};
}
})
The return URI needs to be configured to match for the app in Azure Portal.
Users also need to be assigned, but the internal app is now available anywhere without requiring direct access to the server.

Related

The oauth state was missing or invalid - Unknown location

I am using both Google and Facebook authentication mechanisms in my .NET 7.0 app and they both work fine locally. When deployed in my DEV environment I am getting exceptions when coming back from Google/Facebook. So the challenge works correctly, I am able to authenticate at their side but the callback fails, saying:
Exception: The oauth state was missing or invalid.
Unknown location
I am being redirected to https://my.website.com/signin-google with the state in a querystring parameter. This is the expected behavior as I did not configure an explicit callback path and by default it's set to signin-google and signin-facebook. But somehow it seems like the RemoteAuthenticationHandler does not think this matches the callback path so it's not handling the request? Or would the issue be in the OAuthHandler.HandleRemoteAuthenticateAsync? Maybe when unprotecting the state data? But then why would this work locally?
My setup:
services.AddAuthentication()
.AddGoogle("google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = AppSettings.Instance.GoogleClientId;
options.ClientSecret = AppSettings.Instance.GoogleClientSecret;
})
.AddFacebook("facebook", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.AppId = AppSettings.Instance.FacebookAppId;
options.AppSecret = AppSettings.Instance.FacebookAppSecret;
});
Edit: Could this possibly be linked to the fact I am using 2 servers in my DEV environment and that it uses something machine-related to unprotect the state so it does not work when I land on the other machine?
It turns out this was due to the fact I am now deploying the application on multiple distributed servers, accessed via a load balancer.
This article here helped me to understand how data protection works in ASP.NET Core.
By default key-rotation system is used and the keys are generated by the machine and persisted to %LOCALAPPDATA%\ASP.NET\DataProtection-Keys. So of course this does not work anymore when you scale up your applications.
So instead I decided to store the data protection keys in a shared database so all machines will use the same key:
Add reference to Nuget Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.
Create a DbContext that inherits from IDataProtectionKeyContext
Register your DbContext
Setup your data protection to use the DbContext as:
builder.Services.AddDataProtection()
.PersistKeysToDbContext<DataProtectionKeyContext>();

Microsoft.Identity.Web.UI works locally but not in App Service

I've been trying to add auth to my web app following: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/1-WebApp-OIDC/1-1-MyOrg
If I run locally with appsettings.Development.json via dotnet run, I can log in using my organization credentials as expected. When I containerize and deploy to my Web App in Azure, I do not get logged in successfully. The url in the browser stays at /signin-oidc and goes to the Error page from the default Razor pages app.
The App Service logs have messages saying .AspNetCore.Correlation.OpenIdConnect.[key?] cookie not found
[Update] The auth flow works on my phone but not on desktop.
Why would the same code work locally but not deployed?
Why isn't the cookie found?
Why does it work on iOS but not Windows?
Try to remove app.UseCookiePolicy(); in startup class.
Take a look here
Or the problem was IdentityServer was still using AddDeveloperSigningCredential
You can add a certificate in your code it will work perfectly
Refer here for more info
tl;dr - Setting the same-site cookie options in the authentication settings fixed this.
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
options.NonceCookie.SameSite = SameSiteMode.Unspecified;
options.CorrelationCookie.SameSite = SameSiteMode.Unspecified;
});
This seems to be because the App Service enforces TLS-only and handles the TLS termination. Because of this, requests to the application are always HTTP even though the browser URL is HTTPS. This causes a number of problems, the first was that the redirect URL never matched since the Identity library uses the request scheme to form the redirect URL. I had "fixed" this by injecting a redirect URL rewrite:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
options.Events ??= new OpenIdConnectEvents();
options.Events.OnRedirectToIdentityProvider += _fixRedirect;
});
...
private async Task _fixRedirect(RedirectContext context)
{
context.Request.Scheme = "https";
if(!context.ProtocolMessage.RedirectUri.StartsWith("https"))
context.ProtocolMessage.RedirectUri = context.ProtocolMessage.RedirectUri.Replace("http", "https");
await Task.CompletedTask;
}
However, the correlation cookie also seems to use the same HTTP request so that when the redirect comes back, the HTTPS doesn't match. Relaxing the SameSiteMode explicitly tells the browser(?) to allow the differing schemes. This is why my phone, with a different browser with different cookie policy, worked while desktop did not. Running locally using the dev-certs allowed the schemes to match since the app was terminating the TLS rather than the App Service.
I hooked into the CookiePolicyOptions.OnAppendCookie event to inspect the cookies while debugging to find this out.

Identifying an authenticated user in back-end page requested via window.open

We use IdentityServer4 in our .NET solution, which also includes AspNetCore Web API and Angular front-end app.
There are standard middleware settings to setup identity like app.UseAuthentication() / app.UseAuthorization() / etc. (it's actually an ABP framework-based solution). As a result of these settings, all authentication tokens (access_token, refresh_token, etc.) are stored in Local Storage (I have not found where exactly I can select between Local Storage and other kinds, BTW).
Anyway, it has worked somehow in our DEV environment.
But suddenly the need to use window.open from Angular app popped up. This page is a Hangfire dashboard. Which accesses other resources related to dashboard functionality. And it caused a lot of headache: now, to identify user in server page called from window.open we need to use cookies (URL is not considered of course).
does it mean we have to switch fully from Local Storage to Cookies for storing tokens?
how and where to set it up? Or, if it's not too wild and senseless to just copy existing access_token to Cookies - when and where to do that? My idea was to copy access_token when it is created in Local Storage to Cookies and delete when a user logs off and probably under bunch of different conditions, like browser window is closed, etc. (which ones exactly?)
probably to set refresh_token to be stored in Cookies and read it in the mentioned server page, then obtain access_token by it (if it makes sense at all)
UPDATE: i've finally came up with the following, but it does not work. The idea is: when Angular app makes request to back-end and authentication token is already present - the token needs to be saved to cookies. I see the cookies is added. But later on - on next request - it falls into the condition again, because actually the given cookie is not saved:
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
if (context.SecurityToken is JwtSecurityToken accessToken && !context.HttpContext.Request.Cookies.ContainsKey(accessTokenCookieName))
{
context.HttpContext.Response.Cookies.Append(
accessTokenCookieName,
accessToken.RawData,
new CookieOptions
{
Domain = context.HttpContext.Request.Host.Host,
Path = "/",
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Secure = true,
MaxAge = TimeSpan.FromMinutes(60)
});
}
return Task.CompletedTask;
},
...
}
All apps are web application sharing the same domain. And CORS is set up. AllowCredentials() is called as well when setting up middleware.

signin-oidc Also Tries to Call Internal FQDN

We have an ASP.NET Core application that uses the OpenId Connect authentication protocol. In Azure AD B2C we setup the external FQDN (domain alias) (https://externallink.company.com) as the redirect URI. What we noticed is that after redirecting to https://externallink.company.com/signin-oidc it then redirects to the internal FQDN https://internallink.company.com/signin-oidc which is the URL of the App Service in Azure. The internal link is not accessible outside the company's network and should stay that way.
The error that we got is this:
redirect_uri_mismatch&error_description=AADB2C90006: The redirect URI 'https://internallink.company.com/signin-oidc' provided in the request is not registered for the client id 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx'.%0D%0ACorrelation ID
This is how we have setup the authentication in the Startup class.
Should I specifically put the Callback path so that OpenId does not attempt to call the internal FQDN?
Update:
If I try to add the internal FQDN in the list of redirect URIs I will be able to sign in but afterwards I will get this error (URL is https://internallink.company.com/signin-oidc). This is expected because as I've mentioned the internal URL cannot be accessed via the public internet.
Update 2:
Based from the logs of WAF it does not redirect to https://internallink.com/signin-oidc. After authenticating it goes to https://externallink.com/api/foo which is the desired outcome.
It seems that I was able to solve this problem on my own. I just have to set the ProtocolMessage.RedirectUri under the OnRedirectToIdentityProvider event to the external FQDN.
services.AddAuthentication(options => { ... })
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
...
options.Events.OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.RedirectUri = "https://externallink.com/signin-oidc";
return Task.FromResult(0);
};
});
You need to register in Azure AD the exact redirect URL to use and it should match the redirect URL you sent to Azure AD.
It's a security feature to restrict the allowed redirect-url. Accepting any URL would introduce a major security vulnerability.

Not trying to use /Common endpoint, but Azure AD believes I am

I'm using my organization's Azure AD to authenticate users on a corporate web app. I intend for this to be a single-tenant application. When I run it, I'm prompted to log in with my organization's credentials, as expected. On submitting my credentials, however, I get this error:
Sorry, but we’re having trouble signing you in.
AADSTS50194: Application 'appId'(appname) is not configured as a multi-tenant application. Usage of the /common endpoint is not supported for such applications created after '10/15/2018'. Use a tenant-specific endpoint or configure the application to be multi-tenant.
The thing is, I'm not trying to use the /common endpoint. Here is the relevant bit of my appsettings.json:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "COMPANYDOMAIN.onmicrosoft.com",
"TenantId": "MYTENANTID-xxxx-xxxx-xxxx-xxxx",
"ClientId": "MYCLIENTID-yyyy-yyyy-yyyy-yyyy",
"CallbackPath": "/signin-oidc"
},
And here's the app's settings in the Azure portal (Home>AppRegistrations>App>PlatformConfigurations>Authentication):
My startup.cs, which I assumed set the endpoint in question, is taken directly from the Microsoft-provided sample:
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = false;
});
I've also set false to true for kicks, but the result was the same.
What am I doing wrong, here? Why does Azure AD continue to believe I want to use the /common endpoint?
Edit: As I continued to search, I happened on #jack-jia's answer here: Application is not configured as a multi-tenant application
I haven't quite solved my issue, but their answer offered some promising clues.
Since you have configured it as single tenant application on Azure portal, this issue must occurred due to the wrong authorization endpoint. Please check the value of options.Authority again. If it is identified as single tenant, you can use fiddler to capture the request, the request is something like
GET https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?
client_id=6731de76-14a6-49ae-97bc-6eba6914391e
&response_type=id_token
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
&response_mode=form_post
&scope=openid
&state=12345
&nonce=678910
Check if the endpoint is correct.
I came here with the same error but using python to authenticate my API calls.
Specifically I was using authOAuthDesktopMobileAuthCodeGrant class from bingads.authorization
The solution I ended up with is as follows:
The init for authOAuthDesktopMobileAuthCodeGrant shows the following, note that tenant is defaulted to "common".
def __init__(self, client_id, oauth_tokens=None, env=PRODUCTION, oauth_scope=MSADS_MANAGE, **tenant='common'**):
Therefore, in order to overwrite /common like others here have done. I had to set this tenant component here to my app's tenant ID
authentication = OAuthDesktopMobileAuthCodeGrant(
client_id=client_id,
tenant="{YOUR_TENANT_ID}",
env='production'
Hope this is useful to others who are working out of python.