< Summary - Syki

Information
Class: Syki.Back.Auth.Schemes.SocialLoginScheme
Assembly: Back
File(s): /home/runner/work/syki/syki/Back/Auth/Schemes/SocialLoginScheme.cs
Tag: 97_27801654829
Line coverage
30%
Covered lines: 28
Uncovered lines: 64
Coverable lines: 92
Total lines: 169
Line coverage: 30.4%
Branch coverage
0%
Covered branches: 0
Total branches: 60
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddSocialLoginSchemes(...)100%11100%
HandleRemoteFailure()100%210%
HandleRedirectToAuthorizationEndpoint(...)0%2040%
HandleTicketReceived()0%1640400%
ExtractName(...)0%272160%

File(s)

/home/runner/work/syki/syki/Back/Auth/Schemes/SocialLoginScheme.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Syki.Back.Domain.Identity;
 3using Syki.Back.Domain.Institutions;
 4using Syki.Back.Features.Cross.SignIn;
 5using Microsoft.AspNetCore.Authentication;
 6using Microsoft.AspNetCore.Authentication.OAuth;
 7
 8namespace Syki.Back.Auth.Schemes;
 9
 10public static class SocialLoginScheme
 11{
 12    public const string GoogleScheme = "SocialLogin_Google";
 13
 14    public static AuthenticationBuilder AddSocialLoginSchemes(
 15        this AuthenticationBuilder builder,
 16        IConfiguration configuration)
 17    {
 218        var settings = configuration.SocialLogin;
 19
 220        builder.AddGoogle(GoogleScheme, options =>
 221        {
 222            options.ClientId = settings.Google.ClientId;
 223            options.ClientSecret = settings.Google.ClientSecret;
 224            options.SignInScheme = SocialTempScheme.Name;
 225            options.CallbackPath = "/identity/social-login/callback/google";
 226            options.SaveTokens = false;
 227            options.CorrelationCookie.SameSite = SameSiteMode.Lax;
 228            options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
 229
 230            options.Scope.Clear();
 231            options.Scope.Add("openid");
 232            options.Scope.Add("email");
 233            options.Scope.Add("profile");
 234
 235            options.ClaimActions.MapJsonKey("email_verified", "email_verified");
 236
 237            options.OverrideWith(settings.Google);
 238
 239            options.Events = new OAuthEvents
 240            {
 241                OnRemoteFailure = HandleRemoteFailure,
 242                OnTicketReceived = HandleTicketReceived,
 243                OnRedirectToAuthorizationEndpoint = HandleRedirectToAuthorizationEndpoint,
 244            };
 445        });
 46
 247        return builder;
 48    }
 49
 50    private static async Task HandleRemoteFailure(RemoteFailureContext context)
 51    {
 052        var frontend = context.HttpContext.RequestServices.GetRequiredService<FrontendSettings>();
 053        context.Response.Redirect($"{frontend.Url}?social_login_error={nameof(SocialLoginFailed)}");
 054        context.HandleResponse();
 055    }
 56
 57    private static Task HandleRedirectToAuthorizationEndpoint(RedirectContext<OAuthOptions> context)
 58    {
 059        if (context.Properties.Items.TryGetValue("login_hint", out var loginHint) && loginHint != null)
 60        {
 061            context.RedirectUri += $"&login_hint={Uri.EscapeDataString(loginHint)}";
 62        }
 063        context.Response.Redirect(context.RedirectUri);
 064        return Task.CompletedTask;
 65    }
 66
 67    private static async Task HandleTicketReceived(TicketReceivedContext context)
 68    {
 069        var services = context.HttpContext.RequestServices;
 070        var ctx = services.GetRequiredService<SykiDbContext>();
 071        var signInService = services.GetRequiredService<SignInService>();
 072        var frontendSettings = services.GetRequiredService<FrontendSettings>();
 073        var userManager = services.GetRequiredService<UserManager<SykiUser>>();
 74
 75        // 1. Extract email from OAuth claims
 076        var email = context.Principal?.FindFirst(ClaimTypes.Email)?.Value ?? context.Principal?.FindFirst("email")?.Valu
 77
 078        if (email.IsEmpty())
 79        {
 080            context.Response.Redirect($"{frontendSettings.Url}?social_login_error={nameof(SocialLoginFailed)}");
 081            context.HandleResponse();
 082            return;
 83        }
 84
 085        email = email!.ToLowerInvariant();
 86
 87        // 2. Check email_verified claim (Google provides this)
 088        var emailVerified = context.Principal?.FindFirst("email_verified")?.Value;
 089        if (emailVerified != "true" && emailVerified != "True")
 90        {
 091            context.Response.Redirect($"{frontendSettings.Url}?social_login_error={nameof(SocialLoginEmailNotVerified)}"
 092            context.HandleResponse();
 093            return;
 94        }
 95
 96        // 3. Check if email domain requires corporate SSO
 097        if (await ctx.EmailRequiresSsoAsync(email))
 98        {
 099            context.Response.Redirect($"{frontendSettings.Url}?social_login_error={nameof(SocialLoginSsoRequired)}");
 0100            context.HandleResponse();
 0101            return;
 102        }
 103
 104        // 4. Extract provider info
 0105        var provider = SocialLoginProvider.Google;
 0106        var providerKey = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.Principal?.FindFirst
 107
 108        // 5. Look up existing social login link by (provider, providerKey)
 0109        var existingLink = await ctx.UserSocialLogins.FirstOrDefaultAsync(x => x.Provider == provider && x.ProviderKey =
 0110        if (existingLink != null)
 111        {
 112            // Already linked, generate JWT and login
 0113            await signInService.SignIn(existingLink.Email);
 114
 0115            context.Response.Redirect(frontendSettings.BuildUrl("/home"));
 0116            context.HandleResponse();
 0117            return;
 118        }
 119
 120        // 6. Look up existing user by email (case-insensitive)
 0121        var existingUser = await ctx.Users.FirstOrDefaultAsync(u => u.Email == email);
 0122        if (existingUser != null)
 123        {
 124            // Link social account to existing user
 0125            existingUser.EmailConfirmed = true;
 0126            ctx.Add(new UserSocialLogin(existingUser.Id, provider, providerKey, email));
 0127            await ctx.SaveChangesAsync();
 128
 0129            await signInService.SignIn(email);
 130
 0131            context.Response.Redirect(frontendSettings.BuildUrl("/home"));
 0132            context.HandleResponse();
 0133            return;
 134        }
 135
 136        // 7. Auto-provision new user on public and web schemes
 0137        var name = ExtractName(context.Principal);
 0138        if (name.IsEmpty()) name = email;
 139
 0140        var directorRole = await ctx.GetDirectorRole();
 141
 0142        var institution = Institution.NewForUserRegister();
 0143        var user = new SykiUser(institution, name, email);
 0144        var userRole = new SykiUserRole(institution, user, directorRole.Id);
 0145        var socialLogin = new UserSocialLogin(user.Id, provider, providerKey, email) { User = user };
 146
 0147        ctx.AddRange(institution, userRole, socialLogin);
 0148        await userManager.CreateAsync(user, $"Syki@{Guid.NewGuid()}");
 149
 0150        await signInService.SignIn(email);
 151
 0152        context.Response.Redirect(frontendSettings.BuildUrl("/home"));
 0153        context.HandleResponse();
 0154    }
 155
 156    private static string? ExtractName(ClaimsPrincipal? principal)
 157    {
 0158        if (principal == null) return "User";
 159
 0160        var givenName = principal.FindFirst(ClaimTypes.GivenName)?.Value;
 0161        var surname = principal.FindFirst(ClaimTypes.Surname)?.Value;
 162
 0163        if (givenName.HasValue() && surname.HasValue()) return $"{givenName} {surname}";
 164
 0165        var name = principal.FindFirst(ClaimTypes.Name)?.Value ?? principal.FindFirst("name")?.Value;
 166
 0167        return name;
 168    }
 169}