If all you want to do is sign-in with Google, there's no need for SignInManager
, UserManager
or ASP.NET Core Identity itself. To achieve this, we first need to configure the Authentication services. Here's the relevant code for this, which I'll explain after:
Startup.cs
services
.AddAuthentication(o =>
{
o.DefaultScheme = "Application";
o.DefaultSignInScheme = "External";
})
.AddCookie("Application")
.AddCookie("External")
.AddGoogle(o =>
{
o.ClientId = ...;
o.ClientSecret = ...;
});
The call to AddAuthentication
configures a DefaultScheme
, which ends up being used as both the Application scheme and the Challenge scheme. The Application scheme is used when attempting to authenticate the user (are they signed in?). The Challenge scheme is used when a user is not signed in but the application wants to provide the option to do so. I'll discuss the DefaultSignInScheme
later.
The two calls to AddCookie
add cookie-based authentication schemes for both Application
(our Application scheme) and External
(our SignIn scheme). AddCookie
can also take a second argument, that allows for configuration of e.g. the corresponding cookie's lifetime, etc.
With this in place, the challenge process will redirect the user over to /Account/Login
(by default - this can be configured via the cookie authentication options too). Here's a controller implementation that handles the challenge process (again, I'll explain after):
AccountController.cs
public class AccountController : Controller
{
public IActionResult Login(string returnUrl)
{
return new ChallengeResult(
GoogleDefaults.AuthenticationScheme,
new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(LoginCallback), new { returnUrl })
});
}
public async Task<IActionResult> LoginCallback(string returnUrl)
{
var authenticateResult = await HttpContext.AuthenticateAsync("External");
if (!authenticateResult.Succeeded)
return BadRequest(); // TODO: Handle this better.
var claimsIdentity = new ClaimsIdentity("Application");
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.Email));
await HttpContext.SignInAsync(
"Application",
new ClaimsPrincipal(claimsIdentity));
return LocalRedirect(returnUrl);
}
}
Let's break this down into the two actions:
Login
In order to arrive at the Login
action, the user will have been challenged. This occurs when the user is not signed in using the Application
scheme but is attempting to access a page protected by the Authorize
attribute (or similar). Per your requirement, if the user is not signed in, we want to sign them in using Google. In order to achieve that, we issue a new challenge, this time for the Google
scheme. We do so using a ChallengeResult
that is configured with the Google
scheme and a RedirectUrl
, which is used for returning to our own application code once the Google sign-in process completes. As the code shows, we return to:
LoginCallback
This is where the DefaultSignInScheme
from our call to AddAuthentication
becomes relevant. As part of the Google sign-in process completion, the DefaultSignInScheme
is used for setting a cookie that contains a ClaimsPrincipal
representing the user as returned from Google (this is all handled behind the scenes). The first line of code in LoginCallback
grabs hold of this ClaimsPrincipal
instance, which is wrapped up inside an AuthenticateResult
that is first checked for success. If everything has been successful so far, we end up creating a new ClaimsPrincipal
that contains whatever claims we need (taken from Google in this case) and then signing-in that ClaimsPrincipal
using the Application
scheme. Lastly, we redirect to the page that caused our first challenge.
In response to a couple of follow-up comments/questions in the comments below:
Can I conclude that the SignInManager
and UserManager
are only used when using authentication with a database?
In some ways, yes, I think that's fair. Although it is possible to implement an in-memory store, it doesn't really make much sense with no persistence. However, the real reason not to use these classes in your situation is simply because you do not need a local user account for representing a user. That goes hand-in-hand with persistence, but it's worth making the distinction.
And what very different code from what I read in the book (which I used for setting up my Google login) and all the other answers I've read.
The documentation and the books cover the most common use-case, whereby you do want to store local users that can be linked to external accounts such as Google, etc. If you look at the SignInManager
source, you'll see that it's really just sitting on top of the kind of code I've shown above (e.g. here and here). Other code can be found in the Default UI (e.g. here) and in AddIdentity
.
I assume the LoginCallback gets called by Google. Does the HttpContext.AuthenticateAsync know how to check the data Google sends me? And as it's name is so generic, it looks like it knows how to do that for all external providers?
The call to AuthenticateAsync
here doesn't know anything about Google - the Google-specific handling is configured by the call to AddGoogle
off of AddAuthentication
in ConfigureServices
. After redirecting to Google for sign-in, we actually come back to /signin-google
in our application. Again, this is handled thanks to the call to AddGoogle
, but that code is really just issuing a cookie in the External
scheme that stores the claims that came back from Google and then redirecting to our LoginCallback
endpoint that we configured. If you add a call to AddFacebook
, a /sigin-facebook
endpoint will be configured to do something similar. The call to AuthenticateAsync
is really just rehydrating a ClaimsPrincipal
from the cookie that was created by e.g. the /signin-google
endpoint, in order to retrieve the claims.
It's also worth noting that the Google/Facebook sign-in process is based on the OAuth 2 protocol, so it's kind of generic in itself. If you were to need support for more than just Google, you would just issue the challenge against the required scheme rather than hardcoding it to Google as I've done in the example. It's also possible to add additional properties to the challenge in order to be able to determine which provider was used when your LoginCallback
endpoint is reached.
I've created a GitHub repository that contains a complete example that I built in order to write this answer here.