I'm using trying to use Google authentication in an ASP.NET MVC application.
For testing purposes I'm using the template app generated by VS2013 Update 4
In Google settings the return URLs are properly set and Google+ API is turned on. The app works fine when I publish it to an azure website. I can login using Google accounts without any problems.
However I'd like to deploy it on premises but here we have a reverse proxy setup which works like this:
the server sees itself as server01.mysite.com but this is an
internal name
outside world sees it as www.mysite.com (certain paths are
reverese proxied to the server01.mysite.com
Essentially www.mysite.com/myapp is reverse proxied to server01.mysite.com/myapp
With this setup I can't seem to use Google authentication. GetExternalLoginInfoAsync returns null and the app redirects itself to the login page.
By default the system generates a redirectUri using the private hostname. I tried changing it to the public address but this does not solve the problem.
Below is what I did at startup.auth.cs
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = "...",
ClientSecret = "...",
Provider = new GoogleOAuth2AuthenticationProvider
{
OnApplyRedirect = context =>
{
var redirectUri = context.RedirectUri.Replace("server01", "www");
context.Response.Redirect(redirectUri);
},
}
});
Is there anyway I can make Google authentication work in a setup like this?
Thanks
To achieve this one has to tell the app to use the outside URL earlier so that the relevant hashes are built taking that into account. So instead of changing the redirect URI at the OnApplyRedirect call this before UseGoogleAuthentication:
app.Use((context, next) =>
{
context.Request.Host = new HostString(
context.Request.Host.Value.Replace("server01", "www"));
return next();
}
);
and remove the Provider=... from UseGoogleAuthentication
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = "...",
ClientSecret = "..."
});
Related
I've got an MVC5 Azure web app, which has been working fine with OIDC, using Azure AD as the identity provider.
I'm now trying to put the app behind FrontDoor, but something isn't configured correctly, as the initial request (the one that triggers authenticatation) always redirects to the underlying web app address (app.azurewebsites.net), instead of the FrontDoor (app.azurefd.net). Subsequent requests to the FrontDoor address (that don't need authenticating) work fine.
The web app has an access restriction rule to prevent any access except via the FrontDoor.
The redirect URI configured in the app is set to the FrontDoor address (app.azurefd.net/signin-oidc).
The Azure app registration also has the FrontDoor version (app.azurefd.net/signin-oidc).
The SecurityTokenValidated notification is firing which, if I understand correctly, means that the request from the identity provider back to the redirect URI (app.azurefd.net/signin-oidc) has worked fine, but the final step where it redirects to the URL originally requested is not using the FrontDoor address.
I've tried using FrontDoor Classic and Standard and have tried (with both) having the origin host header match the host name, or being blank. When they match, the behaviour is as described above. Using a blank origin host header is suggested in various places but appears to no-longer work - it now results in a 404.
This GitHub issue describes a very similar problem, but it's using .NET Core, and I'm not sure what the .NET Framework equivalent of the solution is:
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto;
});
My startup.cs class looks like this:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = (context) =>
{
_logger.LogDebug<Startup>("Headers:");
foreach (var header in context.Request.Headers)
{
_logger.LogDebug<Startup>($" {header.Key}: {string.Join(" | ", header.Value)}");
}
var name = context.AuthenticationTicket.Identity.FindFirst("preferred_username").Value;
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, name, string.Empty));
return Task.FromResult(0);
}
}
});
}
I've tried setting the redirect URI using RedirectUri and PostLogoutRedirectUri, but both behave the same.
The FrontDoor origin looks like this:
And the origin group looks like this:
I'm happy to post more config screenshots if that might help, but I'm not sure which bits would be useful.
As far as I can work out, the problem is that the OWIN middleware doesn't use the X-Forwarded-Host from the request, and there doesn't seem to be any option to change this. The .NET Core version makes it easy to configure how headers like this are handled using UseForwardedHeaders (docs), but I couldn't find any equivalent in .NET Framework, so I wrote my own, based on this answer to a similar problem.
public static class UseForwardedHeadersExtension
{
private const string ForwardedHeadersAdded = "ForwardedHeadersAdded";
/// <summary>
/// Checks for the presence of <c>X-Forwarded-Host</c> header, and if present updates the <c>HTTP_HOST</c> header.
/// </summary>
/// <remarks>
/// This extension method is required for running behind FrontDoor. FrontDoor adds the <c>X-Forwarded-Host</c> headers to indicate the host from the original request.
/// </remarks>
public static IAppBuilder UseForwardedHeaders(this IAppBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
// no need to add more than one instance of this middleware to the pipeline.
if (app.Properties.ContainsKey(ForwardedHeadersAdded)) return app;
app.Properties[ForwardedHeadersAdded] = true;
app.Use(async (context, next) =>
{
var request = context.Request;
if (request.Headers.ContainsKey("X-Forwarded-Host"))
{
var httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
var serverVars = httpContext.Request.ServerVariables;
serverVars["HTTP_HOST"] = request.Headers["X-Forwarded-Host"];
}
await next.Invoke().ConfigureAwait(false);
});
return app;
}
}
This is hugely simplistic compared to the .NET Core implementation in ForwardedHeadersExtensions.cs and ForwardedHeadersMiddleware.cs, but given that access to the app is limited to FrontDoor (it can't be accessed directly) it should be safe, as the header will always be set by FrontDoor. There's a great explanation of the security implications in this blog post, which links to this tweet.
Remove the backend origin host header. Leave it blank
I'm attempting to implement the OpenId Connect middleware in a an ASP.NET MVC 5 (.Net Framework) application.
In my AccountController.cs I send an OpenID Connect sing-in request. I have another OpenId connect middleware implemented which is why I specify that the middleware I want to challenge against is "AlternateIdentityProvider".
public void SignIn()
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
"AlternateIdentityProvider");
}
Upon issuing a challenge against the middleware, the RedirectToIdentityProvider event in Startup.cs fires and I am redirected to the provider for sign in. However, after successfully signing in I am redirected to the specified redirect uri with the state and code parameters added as query parameters i.e. http://localhost:63242/singin-oidc/?state=State&code=AuthorizationCode (parameters removed for brevity), which results in a 404 as no such route exists in my application.
Instead I expected the successful signin to trigger the AuthorizationCodeReceived event where I can implement my additional logic. In fact none of the other events ever trigger.
I have implemented an almost identical solution in ASP.Net Core 2.1 and here I am able to step through the different events as they trigger.
The relevant code of my current Startup.cs is shown below. Note that the OpenId provider throws an error if the inital request include reponse_mode and some telemetry parameters, hence these are removed during the initial RedirectToIdentityProvider event.
Any ideas why the callback from the OpenId provider is not getting picked up in the middleware?
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions("AlternateIdentityProvider")
{
ClientId = { { Client Id } },
ClientSecret = { { Client Secret } },
Scope = OpenIdConnectScope.OpenId,
ResponseType = OpenIdConnectResponseType.Code,
RedirectUri = "http://localhost:63242/singin-oidc",
MetadataAddress = { { Discovery document url } },
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = context =>
{
Debug.WriteLine("Redirecting to identity provider for sign in..");
context.ProtocolMessage.EnableTelemetryParameters = false;
context.ProtocolMessage.ResponseMode = null;
return Task.FromResult(0);
},
AuthorizationCodeReceived = context => {
Debug.WriteLine("Authorization code received..");
return Task.FromResult(0);
},
SecurityTokenReceived = context =>
{
Debug.WriteLine("Token response received..");
return Task.FromResult(0);
},
SecurityTokenValidated = context =>
{
Debug.WriteLine("Token validated..");
return Task.FromResult(0);
},
}
});
I was encountering the same issue. I am trying to plug in Owin into our legacy WebForms app.
For me, I had to do the following:
1) Change the application manifest of the application definition on Azure to set the "oauth2AllowIdTokenImplicitFlow" property to true from false.
Go to the Azure Portal,
Select to Azure Active Directory
Select App Registrations
Select your app.
Click on Manifest
Find the value oauth2AllowIdTokenImplicitFlow and change it's value to true
Click Save
2) In your startup.cs file, change the following:
ResponseType = OpenIdConnectResponseType.Code
to
ResponseType = OpenIdConnectResponseType.CodeIdToken
Once, I did those two things, the SecurityTokenValidated and AuthorizationCodeReceived started firing.
Though, I am not sure this is the right way to go or not. Need to do more reading.
Hope this helps.
Please be aware that the OpenId Connect implementation in .Net Framework only support response_mode=form_post. (See closed GitHub issue)
Since you strip the parameter in the request to the OpenId Connect provider (in your RedirectToIdentityProvider notification), then the provider will default to response_mode=query pr. the specs. (see relation between response_type and response_mode ind the specs.)
So in short the OpenId Connect middleware expects that there will come a HTTP POST (with a form body) and your provider will properly send a HTTP GET (with parameters as query-string).
I am trying to setup a new MVC 5 project and a new 2016 server with ADFS. I can authenticate with Oauth 2 and OpenID Connect. The issue I have is when I try to logout of the application. The sign out link points to the default /Account/SignOut action. When that action is called I get redirected to the post logout redirect uri which is https://website.com. This redirect loops until the browser errors out with "website redirected you too many times".
This is my signout method.
public void SignOut()
{
string callbackUrl = Url.Action("SignOutCallback", "Account", routeValues: null, protocol: Request.Url.Scheme);
// Send an OpenID Connect sign-out request.
HttpContext.GetOwinContext().Authentication.SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType
);
}
If I just call
HttpContext.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType)
the logout fails but at least the constant redirects don't happen.
Is there an overload of the Authentication.Signout() method which logs me out and then redirects to the post logout redirect uri?
I worked with MS support and determined that Sign Out is not currently supported for OID Connect with ADFS 2016. It may be part of a future release, but as of now there is no set release date for support.
ADFS 4.0 (on Windows Server 2016) supports "Single log-out". If you use Microsoft.AspNetCore.Authentication.OpenIDConnect package (this is the newer one, for ASP.NET Core) it works seamlessly.
If you query the well known endpoint for OpenID Connect configuration, which is https://your-server/adfs/.well-known/openid-configuration, you will find a key end_session_endpoint which contains the logout URL you should use.
Also, you can check the documentation: https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/development/ad-fs-logout-openid-connect
For instance, in order to invoke it from your controller, you would would use an action method like this one:
[HttpGet]
public IActionResult SignOut()
{
var callbackUrl = Url.Action(nameof(SignedOut), "Account", values: null, protocol: Request.Scheme);
return SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}
You might find this sample useful (even though it is for Azure ADFS, it works for local installs as well): https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore
I have developed an application with ASP.NET MVC5. I have used Facebook external authentication in my application.
When I debug this application with the "Locallhost" domain, the Facebook login works well but when I publish the application in the main server,the AuthenticationManager.GetExternalLoginInfo() returns null and it gives me an error like this in the url:
http://xxxxx.com/Account/ExternalLoginCallback?ReturnUrl=%2Fen&error=access_denied#_=_
I have set the "Site URL" as "http://xxxx.com" and "Valid OAuth redirect URIs" as "http://xxxx.com/signin-facebook" in the Facebook development console.
My setting in the Startup.Outh.cs file is:
var FacebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions();
FacebookOptions.AppId = ConfigurationManager.AppSettings["Facebook_User_Key"];
FacebookOptions.AppSecret = ConfigurationManager.AppSettings["Facebook_Secret_Key"];
FacebookOptions.Provider = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationProvider()
{
OnAuthenticated = async context =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));
foreach (var claim in context.User)
{
var claimType = string.Format("urn:facebook:{0}", claim.Key);
string claimValue = claim.Value.ToString();
if (!context.Identity.HasClaim(claimType, claimValue))
context.Identity.AddClaim(new System.Security.Claims.Claim(claimType, claimValue, "XmlSchemaString", "Facebook"));
}
}
};
FacebookOptions.SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie;
app.UseFacebookAuthentication(FacebookOptions);
I don't know why the external login does not work only in the server with my main domain name. please help me about this problem.
I encountered pretty much the same symptoms you describe:
shortly:
A Facebook authentication worked well on localhost, and after uploading the project to another server (and changing the site URL on Facebook console), authentication did not succeed.
I would recommend you roll back to the MVC template code, and if that works - notice any changes you have made to the middleware code (Startup.Auth.sc).
In particular pay attention to code that interacts with LOCAL configuration, such as Disk I/O and OS permissions for local services.
My particular case:
Starting from the Owin/Katana supported Visual Studio template of a WebAPI project, external login was working perfectly with Facebook, Microsoft and Google OAuth middleware, when testing on localhost.
Later I added come code to Startup.Auth.sc because I needed further authentication activity.
So this was the original code:
public void ConfigureAuth(IAppBuilder app)
{
// see WebAPI template of Visual Studio 2013/2015
...
app.UseFacebookAuthentication(
appId: 99999999,
appSecret: *******);
}
and this was replacement:
public void ConfigureAuth(IAppBuilder app)
{
// see WebAPI template of Visual Studio 2013/2015
...
app.UseFacebookAuthentication(GetFacebookAuth());
}
private FacebookAuthenticationOptions GetFacebookAuth()
{
string picRequest =
String.Format("/me/picture?redirect=false&width={0}&height={0}", ProfileInfoClaimsModel.PIC_SIDE_PX);
var facebookProvider = new FacebookAuthenticationProvider()
{
OnAuthenticated = async (context) =>
{
var client = new FacebookClient(context.AccessToken);
dynamic me = client.Get("/me?fields=id,name,locale");
dynamic mePicture = client.Get(picRequest);
// storing temporary social profile info TO A LOCAL FOLDER
// uploading the local folder to a service WITH A LOCAL CREDENTIAL FILE
...
}
};
var options = new FacebookAuthenticationOptions()
{
AppId = 0123456789,
AppSecret = ******,
Provider = facebookProvider,
};
return options;
}
You may notice that my comments will make the problem obvious - the code points to local resources.
Then I published the project to a virtual server (by Amazon EC2) running Windows Server 2012 with IIS 8.5.
From that moment I kept getting error=access_denied in the redirect from /signin-facebook.
I decided to follow this good old concept, and go back to the original template code. Pretty soon I figured out that I forgot to configure the new server. For instance, the folder the code refers to did not exist and the site had no permission to create it.
Obviously, that solved it.
I'm trying to get ASP.Net MVC 5 Google OAuth2 authentication working correctly.
When I set pass in a GoogleOauth2AuthenticationOptions without any scope, then I'm able to log in successfully.
var googlePlusOptions = new GoogleOAuth2AuthenticationOptions
{
ClientId = googleClientId,
ClientSecret = googleClientSecret,
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = async ctx =>
{
ctx.Identity.AddClaim(new Claim("urn:tokens:googleplus:accesstoken", ctx.AccessToken));
}
},
};
app.UseGoogleAuthentication(googlePlusOptions);
Then this call will return an ExternalLoginInfo object with all the properties set
ExternalLoginInfo loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
When I add any scope though, then I don't get any login info returned. It's just null.
var googlePlusOptions = new GoogleOAuth2AuthenticationOptions
{
ClientId = googleClientId,
ClientSecret = googleClientSecret,
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = async ctx =>
{
ctx.Identity.AddClaim(new Claim("urn:tokens:googleplus:accesstoken", ctx.AccessToken));
}
},
};
googlePlusOptions.Scope.Add(YouTubeService.Scope.Youtube);
app.UseGoogleAuthentication(googlePlusOptions);
Then the call to get external info just returns null.
ExternalLoginInfo loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
In the Google dev console, I have the following APIs turned on..
Analytics API
BigQuery API
Google Cloud SQL
Google Cloud Storage
Google Cloud Storage JSON API
Google+ API
Google+ Domains API
Identity Toolkit API
YouTube Analytics API
YouTube Data API v3
Something about adding scope to the options is breaking GetExternalLoginInfoAsync.
If anyone's still having trouble with this with the latest Microsoft
OWIN middleware (3.0.0+)...
I noticed from Fiddler that by default, the following scope is sent to accounts.google.com:
scope=openid%20profile%20email
If you add your own scope(s) via GoogleOAuth2AuthenticationOptions.Scope.Add(...), then the scope becomes:
scope=YOUR_SCOPES_ONLY
Therefore, you need to add the default scopes too (or at least, this fixed the issue for me):
var googlePlusOptions = new GoogleOAuth2AuthenticationOptions {
...
};
// default scopes
googlePlusOptions.Scope.Add("openid");
googlePlusOptions.Scope.Add("profile");
googlePlusOptions.Scope.Add("email");
// additional scope(s)
googlePlusOptions.Scope.Add("https://www.googleapis.com/auth/youtube.readonly");
So, I figured this out, with a lot of help from http://www.beabigrockstar.com/blog/google-oauth-sign-asp-net-identity. It turns out that the built in Google authentication provider for MVC is openId only. That's why adding a scope broke it. Using Fiddler, I was able to see the GET request to accounts.google.com, which included "scope=openid" in the querystring.
By switching to the GooglePlusOAuth2 provider in the link above, or on Nuget https://www.nuget.org/packages/Owin.Security.GooglePlus and using the provider name of "GooglePlus", I was able to succesfully add the scopes and still get back the login info from GetExternalLoginInfoAsync.
The changes Google has made to their auth mechanisms have been reflected in version 3.0.0 of Microsoft Owin middleware. As you have identified correctly, one of the changes have been moving the OAuth endpoint to Google+ (https://www.googleapis.com/plus/v1/people/me).
So, the key is to:
upgrade the OWIN middleware to version 3.0.0
enable Google+ API for your app in Google Developers Console