I have an ASP.NET Core app that I am integrating with Auth0. After the authentication, I want to redirect to a page to collect information to create a local account, just like the default Facebook and Google extensions do.
I set up a main cookie, an external cookie and my Auth0 point. It then does a callback to the page (/Account/ExternalLogin), where I sign in to the main cookie after doing whatever they need to do, and redirect to a page that requires authorization (/Profile. This all works fine.
However, if I just try to go to that page rather than via the Login route, i get stuck in a loop.
I'm quite sure I'm missing just one stupid thing, but can't seem to get it.
I've tried to pretty much every combination of things I can figure out and have hit the wall. I'm sure it's something stupid.
Here's my relevant part of startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// Add authentication services
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "MainCookie";
options.DefaultChallengeScheme = "Auth0";
})
.AddCookie("MainCookie", options =>
{
options.ForwardChallenge = "Auth0";
})
.AddCookie("External", options =>
{
})
.AddOpenIdConnect("Auth0", options =>
{
// Set the authority to your Auth0 domain
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
// Configure the Auth0 Client ID and Client Secret
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
// Set response type to code
options.ResponseType = "code";
// Configure the scope
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.SignInScheme = "External";
// Set the callback path, so Auth0 will call back to http://localhost:3000/callback
// Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard
options.CallbackPath = new PathString("/callback");
// Configure the Claims Issuer to be Auth0
options.ClaimsIssuer = "Auth0";
options.Events = new OpenIdConnectEvents
{
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
// transform to absolute
var request = context.Request;
postLogoutUri = $"{request.Scheme}://{request.Host}{request.PathBase}{postLogoutUri}";
}
logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri) }";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Profile");
});
}
Here's AccountController
public class AccountController : Controller
{
public async Task Login(string returnUrl = "/")
{
var redirectUrl = Url.Page("/ExternalLogin", pageHandler: "Callback", values: new { returnUrl });
await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties() { RedirectUri = redirectUrl });
}
[Authorize]
public async Task Logout()
{
await HttpContext.SignOutAsync("External");
await HttpContext.SignOutAsync("MainCookie");
await HttpContext.SignOutAsync("Auth0", new AuthenticationProperties
{
RedirectUri = Url.Action("Index", "Home")
});
}
}
SO we redirect to ExternalLogin callback. Currently there is just a submit button that goes to the Confirm callback which completes the login. This will eventually be replaced with a check to see if I have an account for them, and force them to register.
public class ExternalLoginModel : PageModel
{
public IActionResult OnPost(string provider, string returnUrl = null)
{
var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl });
return new ChallengeResult(provider, null);
}
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
return Page();
}
public async Task<IActionResult> OnPostConfirmAsync()
{
var claimsPrincipal = await HttpContext.AuthenticateAsync("External");
await HttpContext.SignInAsync("MainCookie", claimsPrincipal.Principal);
await HttpContext.SignOutAsync("External");
return RedirectToPage("/Profile");
}
}
So when I go /Account/Login, it correctly sends me to Auth0, then to ExternalLogin, and I can click the button and set the Main Cookie. This then lets me access /Profile.
However, If I'm not already authorized, If I execute /Profile, I then kick over to Auth0, but after authenticating I just get stuck in a loop like this.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44375/profile
Microsoft.AspNetCore.Routing.EndpointMiddleware:Information: Executing endpoint 'Page: /Profile'
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Route matched with {page = "/Profile", action = "", controller = ""}. Executing page /Profile
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed.
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes ().
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler:Information: AuthenticationScheme: Auth0 was challenged.
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Executed page /Profile in 11.2594ms
Microsoft.AspNetCore.Routing.EndpointMiddleware:Information: Executed endpoint 'Page: /Profile'
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 28.548ms 302
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 POST https://localhost:44375/callback application/x-www-form-urlencoded 375
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler:Information: AuthenticationScheme: External signed in.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 113.1223ms 302
Changing options.DefaultChallengeScheme = "Auth0" to options.DefaultChallengeScheme = "MainCookie" was all that was needed.
Related
I have project to use SignalR from desktop application. So my application have 2 method authentication. the one is cookie for web, the one is jwt for signalr client. cause the client comes from desktop.
How to exactly authentication, i check header request, client sent
Authorization: Bearer testing
in server, i assigns the token to MessageReceivedContext.Token. so i think, it automatically handle by some magic in background by AuthorizeAttribute. the description of property Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location.
Client.js
"use strict";
var connection = new signalR.HubConnectionBuilder()
.withUrl("/socket", {
accessTokenFactory: () => "testing"
})
.build();
connection.start();
Hub.cs
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class SocketHub : Hub
{
public override Task OnConnectedAsync()
{
return Task.Run(()
=> Console.WriteLine(Context.ConnectionId));
}
}
Program.cs
AddJwtBearer(options =>
{
options.Authority = "http://localhost:5000/socket";
options.RequireHttpsMetadata = false;
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Headers.Authorization;
// If the request is for our hub...
if (!string.IsNullOrEmpty(accessToken))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
I have setup Asp.Net Identity in my application, and would like to have the following setup:
Have a selfmade username&password authentication as IdentityConstants.ApplicationScheme
Have external providers (e.g. Google, Facebook) which authenticate as IdentityConstants.ExternalScheme
My social login looks like this:
[HttpGet]
[Route("ExternalLogin")]
public IActionResult ExternalLogin([FromQuery] string provider, [FromQuery] bool rememberMe)
{
var redirectUrl = Url.Action("SocialLoginCallback", "SocialLogin", new
{
rememberMe
});
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
[HttpGet]
[Route("SocialLoginCallback")]
public async Task<IActionResult> SocialLoginCallback([FromQuery] bool rememberMe)
{
// Grab the external login information from the http context
var loginInfo = await _signInManager.GetExternalLoginInfoAsync();
if (loginInfo is null)
{
return Problem();
}
var signinResult = await _signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, rememberMe, true);
if (signinResult.Succeeded)
{
return Ok();
}
return Ok();
}
My startup google auth looks like:
authBuilder.AddGoogle(options =>
{
options.ClientId = Configuration["GoogleClientId"];
options.ClientSecret = Configuration["GoogleClientSecret"];
options.SignInScheme = IdentityConstants.ExternalScheme;
});
However, I noticed that the login is still executed with IdentityConstants.ApplicationScheme.
After examining why, the problem seems to stem from the var signinResult = await _signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, rememberMe, true); call. Internally this runs into a call to await Context.SignInAsync(IdentityConstants.ApplicationScheme, userPrincipal, authenticationProperties ?? new AuthenticationProperties());, which has the ApplicationScheme hardcoded.
How can I get this to work as I would like to have?
I would like to distinguish both logins in a middleware, and as of now, the only way to do this would be to check the respective claims, but I'd rather just to a simple check which auth scheme the login is using. Is this possible?
If you want to set the default authentication method ,You can set DefaultScheme in ConfigureServices/Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
//......
services.AddAuthentication(option =>
{
option.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(option =>
{
option.LoginPath = "/Identity/Account/Login";
})
.AddGoogle(option => {
option.ClientId = "xxx";
option.ClientSecret = "xxx";
});
}
I set the login page as the default Identity login page, when I access the action whitch has [Authorize] attribute , the login page has two ways to log in.
If the app have multiple instances of an authentication handler , You can use [Authorize(AuthenticationSchemes = xxxx)] to Select the scheme with the Authorize attribute.
You can read this document to learn more details about Authorize with a specific scheme in ASP.NET Core
We are trying to understand how the authentication cookies (ASP.NET Core 5.0 - Microsoft.AspNetCore.Authentication.OpenIdConnect version 5.0.11) work with the Authorization Code Flow without PKCE.
Auth Process
The auth process looks like this: the login in the frontend redirects to the login endpoint of the AuthController and starts the OpenId Connect process. So you are authenticated by the Identity Provider and the cookies are set for the user. Which are sent with every call of the API to check if the request is authenticated.
3 cookies are created in the process:
Cookie #1:
Name = .AspNetCore.Cookies
Value = chunks-2
Cookie #2:
Name = .AspNetCore.CookiesC1
Value = CfDJ8GRK-GHfascFTvp0o_E7oKZU-6GOAbUGCPHZZPfewEv12PmKgr46gfeTQC351e-Jnxq8SxzjJEgboIedIPCO11Q […]
Cookie #3:
Name = .AspNetCore.CookiesC2
Value = 8G86qN27NOS2Z-75XqY34d-ID1nOELpPaHUIe2EkFZMmfjrYSKA2JaU30p4Ozh8RyxZXTpFCRV8
Questions
How are these .AspNetCore cookies used for authentication?
How are the names generated and the value encrypted?
What does these cookies contain?
We tried to decrypt the cookie (How to manually decrypt an ASP.NET Core Authentication cookie?) to understand how it works but this did not work for us.
Unfortunately, we have not yet found an answer as to how the cookie is generated (with name and value) in theory.
I hope the questions were understandable and I would appreciate if someone could answer them.
Code snippets for a better understanding. Hopefully :)
AuthController:
// https://auth0.com/blog/backend-for-frontend-pattern-with-auth0-and-dotnet/
public class AuthController : Controller
{
public ActionResult Login(string returnUrl = "/login")
{
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties() { RedirectUri = returnUrl });
}
[Authorize]
public async Task<ActionResult> Logout()
{
await HttpContext.SignOutAsync();
return new SignOutResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties
{
//RedirectUri = Url.Action("Index", "Home")
RedirectUri = "/logout"
});
}
//[Authorize]
public ActionResult GetUser()
{
var jsonReturn = new Dictionary<string, string>();
if (User != null && User.Identity.IsAuthenticated)
{
jsonReturn.Add("isAuthenticated", "true");
foreach (var claim in ((ClaimsIdentity)this.User.Identity).Claims)
{
jsonReturn.Add(claim.Type, claim.Value);
}
return Json(JsonConvert.SerializeObject(jsonReturn));
}
jsonReturn.Add("isAuthenticated", "false");
return Json(JsonConvert.SerializeObject(jsonReturn));
}
}
Startup:
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(o =>
{
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
o.Cookie.SameSite = SameSiteMode.Strict;
o.Cookie.HttpOnly = true;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => ConfigureOpenIdConnect(options));
}
private void ConfigureOpenIdConnect(OpenIdConnectOptions options)
{
options.Authority = <identity provider url>;
options.ClientId = "<clientId>";
options.ClientSecret = "<clientSecret>";
options.ResponseMode = OpenIdConnectResponseMode.FormPost;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("offline_access");
options.CallbackPath = new PathString("/callback");
options.SaveTokens = true;
options.UseTokenLifetime = false;
}
The .AspNetCore cookie is created by the Cookie authentication handler after the user has successfully authenticated (being challenged) with the OpenIDConnect handler.
If The cookie size is to big, then it will be broken up into chunks of 4Kb to make sure the cookies don't get rejected the browser or proxies.
The data inside the cookies is encrypted using the Data Protection API and with some effort you can decrypt the content of the cookie using the Data Protection aPI.
the data inside the cookie contains mainly of your ClaimsPrincipal (The user objects) with its various claims. Optionally you can also store your openid-connect tokens inside the cookie.
Hope this answers your questions.
I have an ASP.NET Core 2.1 MVC application and I'm trying to use Azure AD to authenticate. The application redirects to the Microsoft login page but when I logout and then go back to the homepage of the application it automatically logs back in.
I've tried calling https://login.microsoftonline.com/common/oauth2/v2.0/logout and clearing the cookies but to no avail.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = false;
options.Events.OnRedirectToIdentityProviderForSignOut = async context =>
{
var h = new HttpClient();
var r = await h.GetAsync($"https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=https%3A%2F%2Flocalhost%2%3A5001%2F");
foreach (var cookie in context.Request.Cookies.Keys)
{
context.Response.Cookies.Delete(cookie);
}
};
options.Events.OnTokenResponseReceived = async conext =>
{
var t = 1;
};
});
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public async Task<IActionResult> Logout()
{
var result = SignOut("AzureAD", "AzureADOpenID", "AzureADCookie");
return result;
}
Please check my way to add Azure AD authentication to ASP.NET Core 2.1 MVC application. The tool will add the authentication code for you. What you need to do is binding your sign in/out button to the method.
1.Click Connected Services->choose Authentication with Azure Active Directory.
2.You need to provide a login button for trigger the login page.
3.Input your tenant name for Domain and choose a way for providing application settings.
4.Click finish button to complete the configuration.
5.Delete app.UseBrowserLink() in Startup.cs.
6.Call SignOut() method in AccountController.cs to sign out the user. It works well.
[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);
}
Since you are using the Microsoft.AspNetCore.Authentication.AzureAD.UI library , you can directly redirect user to https://localhost:xxxxx/AzureAD/Account/SignOut for sign out , Source code :
[HttpGet("{scheme?}")]
public IActionResult SignOut([FromRoute] string scheme)
{
scheme = scheme ?? AzureADDefaults.AuthenticationScheme;
var options = Options.Get(scheme);
var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme);
return SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
options.CookieSchemeName,
options.OpenIdConnectSchemeName);
}
You can now remove the OnRedirectToIdentityProviderForSignOut event .
In .NET Core, signing out of Active Directory is now built into the provided Account controller > SignOut action. You can provide a button like this:
<a asp-area="AzureAD" asp-controller="Account" asp-action="SignOut">Sign out</a>
This generates the url: /AzureAD/Account/SignOut
https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/1-WebApp-OIDC/1-6-SignOut
I have started looking into Google signin and have added the normal provider as such.
ddGoogle(go =>
{
go.ClientId = "xxxxx";
go.ClientSecret = "-xxxxx";
go.SignInScheme = IdentityConstants.ExternalScheme;
});
My test method just to get it started looks like this
public ActionResult TestGoogle()
{
var redirectUrl = Url.Action(nameof(ExternalCallback), "Account", new { ReturnUrl = "" });
var properties = _signInManager.ConfigureExternalAuthenticationProperties("Google", redirectUrl);
return Challenge(properties, "Google");
}
All well and good I go to google Log in and get redirected with all required claims as expected.
The issue is when I call _signInManager.SignOutAsync() which does not seem to do anything. No errors, yet when I go back to my TestGoogle action I am redirected with all credentials to the callback.
Anything I am missing?
In your Logout Action add this code
return Redirect("https://www.google.com/accounts/Logout?continue=https://appengine.google.com/_ah/logout?continue=[[your_return_url_here]]");
This is how I configured my code:
Configure 2 Cookies, one (MainCookie) for local login and second (ExternalCookie) for google.
services.AddAuthentication("MainCookie").AddCookie("MainCookie", options =>
{
});
services.AddAuthentication("ExternalCookie").AddCookie("ExternalCookie", o =>
{
});
Configure google authentication as shown below:
services.AddAuthentication(
v =>
{
v.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
v.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).
AddGoogle("Google", googleOptions =>
{
googleOptions.ClientId = "xxx...";
googleOptions.ClientSecret = "zzz...";
googleOptions.SignInScheme = "ExternalCookie";
googleOptions.Events = new OAuthEvents
{
OnRedirectToAuthorizationEndpoint = context =>
{
context.Response.Redirect(context.RedirectUri + "&hd=" + System.Net.WebUtility.UrlEncode("gmail.com"));
return Task.CompletedTask;
}
};
});
TestGoogle() Method will redirect you to google login page.
You can then get the claims from google back like so:
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
var info = await HttpContext.AuthenticateAsync("ExternalCookie");
//Sign in to local cookie and logout of external cookie
await HttpContext.SignInAsync("MainCookie", info.Principal);
await HttpContext.SignOutAsync("ExternalCookie");
//ExternalCookie will be deleted at this point.
return RedirectToLocal(returnUrl);
}
If you want to now want to authenticate any method, you can do so as shown below:
[Authorize(AuthenticationSchemes = "MainCookie")]
public async Task<IActionResult> Contact()
{
//Only authenticated users are allowed.
}