I got a response to my ticket last month, but it wasn’t immediately clear to me what the recommendation was, and I have been working on other app features since then. I was able to get back to this today, and got it working.
To do this, you’ll want to make use of standard ASP.NET infrastructure to sign in the user server-side. My current solution may not be perfect, but it works.
-
After a user completes the sign-up process on my custom sign up form, I redirect them to a new endpoint on my secured application (e.g. https://mywebapp.com/Account/autoLogin). This endpoint is setup to allow anonymous requests, so the user is not prompted for login credentials via the Auth0 login form. I have the sign up form pass an authorization query parameter to this endpoint, which contains the user’s username and password.
-
This authLogin endpoint parses the user credentials passed into it and calls the GetToken method in the Auth0 Authentication API (Resource Owner Password flow).
-
If the GetToken method returns a populated AccessToken, the endpoint creates a Cookie-based ClaimsIdentity with the user’s username, and calls HttpContext.SignInAsync. SignInAsync generates the Authentication Cookies that Auth0 needs to confirm user identity (Everything needed for it to do this is pre-setup in Startup.cs, as per the standard Auth0 + ASP.NET integration).
-
With the cookies generated, the user is redirected to the secured site home page, in a logged-in state, with no login form presented. Works like a charm.
Some notes before I include some code for anyone interested:
-
You’ll want to use the ‘auth0-forwarded-for’ parameter to send Auth0 the client’s IP address when calling GetToken, as per their recommendation. Just be careful with this, since client IPs can be spoofed by evil-dooers. A well-configured proxy in front of your secured site will help with this. I’m still getting this part properly secured, so treat that part of the code below with a grain of salt. Further reading on this here and here.
-
I’m sure there are better ways to setup this server-side, cookie-based authorization/authentication. ASP.NET has some cool options with adding Middleware to structure this kind of thing better (info here)
-
I have not completed security hardening this code, there are definitely some issues with it. Although untested, I may change everything around so my custom signup form is setup with everything it needs to perform a HttpContext.SignInAsync and generate the Auth0 cookies directly, to reduce the number of handoffs the user’s credentials endures. Also, I’m only passing the user’s credentials via a query parameter because I was having CORS/cross-site security issues with using the Authorization Header; the Header would be the preferred mechanism. It’s probably not great to have the password stored in a string instead of a SecuredString, too.
Anyway, here you go:
[AllowAnonymous]
public async Task<IActionResult> autoLogin([FromQuery] string authorization)
{
//TODO - Can all of this be done in Sign Up form, instead of Secured Site, so no need for all this crazy passing around of credentials?
string[] credentials = getCredentialsFromHeader(authorization);
bool authorizedAttempt = await authorizeBasicLoginAttempt(credentials, HttpContext);
if (authorizedAttempt)
{
await logInUser(credentials[0], HttpContext);
}
return Redirect(Url.Action("home", "HomePage"));
}
private static string[] getCredentialsFromHeader(string authorizationHeader)
{
string[] credentials = new string[] { "", "" };
if (authorizationHeader != null && authorizationHeader.StartsWith("Basic "))
{
string decodedCredentials = (getBasicAuthDecodedCredentials(authorizationHeader));
credentials = splitBasicAuthDecodedCredentials(decodedCredentials);
}
return credentials;
}
private static string getBasicAuthDecodedCredentials(string authorizationHeader)
{
string encodedCredentials = authorizationHeader.Replace("Basic ", "");
byte[] decodedCredentialBytes = Convert.FromBase64String(encodedCredentials);
return Encoding.UTF8.GetString(decodedCredentialBytes);
}
private static string[] splitBasicAuthDecodedCredentials(string decodedCredentials)
{
string[] splitCredentials = new string[] { "", "" };
int credentialSeperatorIndex = decodedCredentials.IndexOf(':');
if (credentialSeperatorIndex > -1)
{
splitCredentials[0] = decodedCredentials.Substring(0, credentialSeperatorIndex);
splitCredentials[1] = decodedCredentials.Substring(credentialSeperatorIndex + 1);
}
return splitCredentials;
}
private static async Task<bool> authorizeBasicLoginAttempt(string[] credentials, HttpContext httpContext)
{
bool authorized = false;
//TODO - Research the security behind this and improve as needed. Getting the actual client IP is a little tricky, can be spoofed. Cloudflare has a "True-Client-IP header for enterprise users. Other options?
//https://stackoverflow.com/questions/735350/how-to-get-a-users-client-ip-address-in-asp-net
//https://stackoverflow.com/questions/1907195/how-to-get-ip-address/13249280#13249280
string clientIpAddress = httpContext.Connection.RemoteIpAddress.ToString();
ResourceOwnerTokenRequest authenticationRequest = buildResourceOwnerAuthenticationRequest(credentials, clientIpAddress);
AuthenticationApiClient authenticationClient = new AuthenticationApiClient(AUTH0_DOMAIN);
try
{
AccessTokenResponse authenticationResponse = await authenticationClient.GetTokenAsync(authenticationRequest);
if (String.IsNullOrEmpty(authenticationResponse.AccessToken) == false)
{
authorized = true;
}
} catch (AuthenticationException ae)
{
Console.WriteLine(ae.ToString());
}
return authorized;
}
private static ResourceOwnerTokenRequest buildResourceOwnerAuthenticationRequest(string[] credentials, string clientIpAddress)
{
ResourceOwnerTokenRequest resourceOwnerAuthenticationRequest = new ResourceOwnerTokenRequest();
resourceOwnerAuthenticationRequest.ClientId = AUTH0_CLIENT_ID;
resourceOwnerAuthenticationRequest.ClientSecret = AUTH0_CLIENT_SECRET;
resourceOwnerAuthenticationRequest.ForwardedForIp = clientIpAddress;
resourceOwnerAuthenticationRequest.Username = credentials[0];
resourceOwnerAuthenticationRequest.Password = credentials[1];
return resourceOwnerAuthenticationRequest;
}
private static async Task logInUser(string username, HttpContext httpContext)
{
List<Claim> userClaims = new List<Claim>();
userClaims.Add(new Claim(ClaimTypes.Name, username));
userClaims.Add(new Claim(ClaimTypes.Email, username));
ClaimsIdentity userClaimsIdentity = new ClaimsIdentity(userClaims, CookieAuthenticationDefaults.AuthenticationScheme);
await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(userClaimsIdentity), new AuthenticationProperties());
}