ASP.NET Core authenticate with existing authentication ticket in OAuth provider OnCreatingTicket event callback

iKingNinja 60 Reputation points
2024-09-19T11:14:42.4466667+00:00

I have cookie authentication with 2 OAuth2 providers set up. The way it works is the user creates an account on my website with Discord OAuth2 and can then link their ExampleSite account with OAuth2 as well.

This is my authentication set up:


builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OAuthAuthenticationDefaults.DiscordAuthenticationScheme;
})
    .AddCookie(options =>
    {
        options.Cookie = new()
        {
            Name = "auth",
            SameSite = SameSiteMode.Lax,
            Path = "/",
            HttpOnly = true,
            IsEssential = true,
            MaxAge = TimeSpan.FromDays(6 * 30)
        };

        options.ExpireTimeSpan = TimeSpan.FromDays(6 * 30);
    })
    .AddOAuth(OAuthAuthenticationDefaults.DiscordAuthenticationScheme, options =>
    {
        options.ClientId = configuration["Credentials:Discord:ClientId"] ??
            throw new ArgumentNullException(null, "Discord client ID was null");
        options.ClientSecret = configuration["Credentials:Discord:ClientSecret"] ??
            throw new ArgumentNullException(null, "Discord client secret was null");
        options.AuthorizationEndpoint = "https://discord.com/oauth2/authorize";
        options.CallbackPath = configuration["OAuth2:Discord:RedirectUri"] ??
            throw new ArgumentNullException(null, "Discord OAuth2 redirect URI was null");

        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.Scope.Add("identify");

        options.CorrelationCookie = new()
        {
            Name = "correlation.discord."
        };

        options.Events.OnCreatingTicket = async context =>
        {
            HttpRequestMessage req = new(HttpMethod.Get, context.Options.UserInformationEndpoint);
            req.Headers.Authorization = new("Bearer", context.AccessToken);

            HttpResponseMessage res = await context.Backchannel.SendAsync(req);
            res.EnsureSuccessStatusCode();

            string userData = await res.Content.ReadAsStringAsync();
            DiscordUserInformation? discordUser = JsonSerializer.Deserialize<DiscordUserInformation>(userData) ??
                throw new NullReferenceException(nameof(discordUser));

            context.RunClaimActions(JsonDocument.Parse(userData).RootElement);

            // Check if user already exists

            IUserManager userManager = context.HttpContext.RequestServices.GetRequiredService<IUserManager>();
            ApplicationUser? user = await userManager.GetApplicationUserAsync(ulong.Parse(discordUser.Id));

            if (user != null) return;

            // Save user to database

            await userManager.CreateAsync(ulong.Parse(discordUser.Id), []);
        };
    })
    .AddOAuth(OAuthAuthenticationDefaults.ExampleSiteAuthenticationScheme, options =>
    {
        options.ClientId = configuration["Credentials:ExampleSite:ClientId"] ??
            throw new ArgumentNullException(null, "ExampleSite client ID was null");
        options.ClientSecret = configuration["Credentials:ExampleSite:ClientSecret"] ??
            throw new ArgumentNullException(null, "ExampleSite client secret was null");

        options.AuthorizationEndpoint = "https://apis.ExampleSite.com/oauth/v1/authorize";
        options.CallbackPath = configuration["OAuth2:ExampleSite:RedirectUri"] ??
            throw new ArgumentNullException(null, "ExampleSite OAuth2 redirect URI was null");
        options.TokenEndpoint = "https://apis.ExampleSite.com/oauth/v1/token";
        options.UserInformationEndpoint = "https://apis.ExampleSite.com/oauth/v1/userinfo";

        options.ClaimActions.MapJsonKey("urn:ExampleSite:id", "sub");
        options.ClaimActions.MapJsonKey("urn:ExampleSite:username", "preferred_username");

        options.Scope.Add("openid");
        options.Scope.Add("profile");

        options.CorrelationCookie = new()
        {
            Name = "correlation.ExampleSite."
        };

        options.Events.OnCreatingTicket = async context =>
        {
            HttpRequestMessage req = new(HttpMethod.Get, context.Options.UserInformationEndpoint);
            req.Headers.Authorization = new("Bearer", context.AccessToken);

            HttpResponseMessage res = await context.Backchannel.SendAsync(req);
            res.EnsureSuccessStatusCode();

            string userData = await res.Content.ReadAsStringAsync();
            ExampleSiteUserInformation ExampleSiteUser = JsonSerializer.Deserialize<ExampleSiteUserInformation>(userData) ??
                throw new NullReferenceException(nameof(ExampleSiteUser));

            IUserManager userManager = context.HttpContext.RequestServices.GetRequiredService<IUserManager>();

// context.HttpContext.User.Identity.IsAuthenticated is false while it is true if I access the User object from a controller
            ApplicationUser user = await userManager.GetApplicationUserAsync(ulong.Parse(context.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value)) ??
                throw new NullReferenceException(nameof(user));

            // Check if the user already verified this account

            ulong ExampleSiteUserId = ulong.Parse(ExampleSiteUser.Id);

            if (await userManager.GetUserExampleSiteVerificationAsync(ExampleSiteUserId) != null) return;

            context.RunClaimActions(JsonDocument.Parse(userData).RootElement);

            await userManager.VerifyExampleSiteUserAsync(user.Id, ExampleSiteUserId);
        };
    });

The issue lies here

ulong.Parse(context.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value)

context.HttpContext.User.Identity.IsAuthenticated is false even though the user previously authenticated with Discord and the value is true when accessed from a controller.

I tried explicitly setting OAuthOptions.SignInScheme so that the authentication is performed with the data stored in the cookie but it didn't make any difference.

ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,526 questions
0 comments No comments
{count} votes

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.