Please consider the following code in program.cs:
var builder = WebApplication.CreateBuilder(args);
//omit some builder settings
builder.Services.AddAuthentication(options =>
{
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/signin";
options.LogoutPath = "/signout";
})
.AddOAuth("Discord", options =>
{
options.AuthorizationEndpoint = "https://discord.com/api/oauth2/authorize";
options.Scope.Add("identify");
options.Scope.Add("email");
options.CallbackPath = new PathString("/Account/LoginCallback");
options.ClientId = "some id";
options.ClientSecret = "some secret";
options.TokenEndpoint = "https://discord.com/api/oauth2/token";
options.UserInformationEndpoint = "https://discord.com/api/users/@me";
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "username");
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
options.ClaimActions.MapCustomJson("urn:discord:avatar:url", user =>
string.Format(
CultureInfo.InvariantCulture,
"https://cdn.discordapp.com/avatars/{0}/{1}.{2}",
user.GetString("id"),
user.GetString("avatar"),
user.GetString("avatar")!.StartsWith("a_") ? "gif" : "png"));
options.AccessDeniedPath = "/Account/LoginFailed";
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
context.RunClaimActions(user);
}
};
});
The following is inside my account controller:
[HttpGet("~/signin")]
public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync());
[HttpPost("~/signin")]
public async Task<IActionResult> SignIn([FromForm] string provider)
{
if (string.IsNullOrWhiteSpace(provider))
{
return BadRequest();
}
if (!await HttpContext.IsProviderSupportedAsync(provider))
{
return BadRequest();
}
return Challenge(new AuthenticationProperties {RedirectUri = "/Account/LoginCallback" }, provider);
}
[HttpGet("~/signout")]
[HttpPost("~/signout")]
public IActionResult SignOutCurrentUser()
{
return SignOut(new AuthenticationProperties {RedirectUri = "/"},
CookieAuthenticationDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult LoginCallback()
{
return RedirectToAction("Index", "Home");
}
The above code works. What happens is the following:
- I click on LoginViaDiscord button
- SignIn post method is called
- I am redirected to discord
- I Login through discord
- I get redirected back to LoginCallback
When I look at the website, I am all fully logged in. The problem is that I have skipped that whole database step where user gets recorded and an account is created.
When I did similar authentication with MVC 5 I would have a callback action method and that callback method would do all the setup. Create the user, do logic around the user and then sign them in. However, with this code I am already fully logged in and there was no database step involved.
My question is how to properly accomplish same as above but not be logged in when the challenge returns to LoginCallback and how to retrieve the information from discord inside LoginCallback action? I would like to perform some logic there on the user before allowing for them to be logged in and I also want to ensure their account is created in the database.