Silent Auto-Login for User after Completing 'Authentication API'-Backed Custom Signup Form

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.

  1. 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.

  2. This authLogin endpoint parses the user credentials passed into it and calls the GetToken method in the Auth0 Authentication API (Resource Owner Password flow).

  3. 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).

  4. 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());

	}
1 Like