Search code examples
c#asp.net-coreauthenticationasp.net-web-apioauth-2.0

How to register in local IdentityContext the users, obtained from external authentication provider


For my API, i would like to enable OAuth 2.0 authentication at external login provider. But, once user connects to my API, i would like to create a user account, to store there some user properties. Or access this information from my variation of IdentityDBContext. My principal question: how can I check is user with given email was created, and create a profile if not existent? Here my code in Startup.ConfigureServices for LinkedIn OAuth 2.0 provider:

services.AddDbContext<LookForJobIdentityContext>(options =>
        {
            options.UseSqlServer(Configuration.GetConnectionString("IdentityDB"));
        });
        services.AddIdentity<LookForJobUser, LookForJobRole>()
            .AddEntityFrameworkStores<LookForJobIdentityContext>()
            .AddDefaultTokenProviders()
            .AddSignInManager();
        services.AddAuthentication(defaults =>
        {
            defaults.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            defaults.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        })
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions =>
        {
            configureOptions.LoginPath = new PathString("/login");
            configureOptions.LogoutPath = new PathString("/logout");
        })
        .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, config =>
        {
            config.TokenValidationParameters = new TokenValidationParameters()
            {
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = false,
                ValidIssuer = Configuration.GetValue<string>("Jwt:Issuer"),
                ValidAudience = Configuration.GetValue<string>("Jwt:Issuer"),
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetValue<string>("JwtKey")))
            };
        })
        .AddOAuth("LinkedIn", scheme =>
        {
            scheme.ClientId = Configuration.GetValue<string>("LinkedIn:ClientId");
            scheme.ClientSecret = Configuration.GetValue<string>("LinkedIn:ClientSecret");
            scheme.Scope.Add("r_liteprofile");
            scheme.Scope.Add("r_emailaddress");
            scheme.CallbackPath = new PathString("/signin-linkedin");
            //scheme.CallbackPath = new PathString("/api/SignIn/linkedin-signin");
            scheme.AuthorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization";
            scheme.TokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
            scheme.UserInformationEndpoint = "https://api.linkedin.com/v2/me?projection=(id,firstName,lastName)";
            scheme.Events = new OAuthEvents()
            {
                OnCreatingTicket = async context =>
                {
                    var userDataRequest = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
                    userDataRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
                    userDataRequest.Headers.Add("x-li-format", "json"); // Tell LinkedIn we want the result in JSON, otherwise it will
                                                                        // return XML
                    var userDataResponce = context.Backchannel.SendAsync(userDataRequest, context.HttpContext.RequestAborted);

                    var userEmailRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))");
                    userEmailRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
                    userEmailRequest.Headers.Add("x-li-format", "json");


                    var userEmailResponce = context.Backchannel.SendAsync(userEmailRequest, context.HttpContext.RequestAborted);

                    (await userDataResponce).EnsureSuccessStatusCode();
                    (await userEmailResponce).EnsureSuccessStatusCode();

                    var userDataResponceResult = userDataResponce.Result;
                    var userEmailResponceResult = userEmailResponce.Result;
                    // Extract the user info object
                    var userData = JObject.Parse(await userDataResponceResult.Content.ReadAsStringAsync());
                    var userEmail = JObject.Parse(await userEmailResponceResult.Content.ReadAsStringAsync());
                    string userId = userData.Value<string>("id");
                    string lastaname = userData["lastName"]?["localized"]?.Value<string>("en_US");
                    string firstName = userData["firstName"]?["localized"]?.Value<string>("en_US");
                    string email = userEmail["elements"]?[0]?["handle~"]?.Value<string>("emailAddress");

                    var claims = new List<Claim>();

                    if (!String.IsNullOrEmpty(userId))
                        claims.Add(new Claim(ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                    if (!String.IsNullOrEmpty(lastaname))
                        claims.Add(new Claim(ClaimTypes.Name, lastaname, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                    if (!String.IsNullOrEmpty(firstName))
                        claims.Add(new Claim(ClaimTypes.GivenName, firstName, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                    if (!String.IsNullOrEmpty(email))
                        claims.Add(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                    var claimsIdentity = new ClaimsIdentity(claims, context.Scheme.Name);
                    await context.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
                    context.RunClaimActions();
                }
            };
        });

I dont want to create a copy of ServiceProvider inside OnCreatingTicket event handler. May be there is more elegant way to achieve it. If you know, please give me the idea. Thank you!


Solution

  • I dont want to create a copy of ServiceProvider inside OnCreatingTicket event handler

    If I get the question right, we need some mechanism to access UserManager in order to create or doing something with the user account as we wish. Ideally should be accessible from OnCreatingTicket (as you mention earlier), without create a copy of ServiceProvider. So, how about using the HttpContext to achieve what we need ?

    This block code just describe the idea

    services.AddAuthentication().AddOAuth("linkedIn", scheme =>
    {
        scheme.Events = new OAuthEvents
        {
            OnCreatingTicket = async context =>
            {
                var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
                // do whatever it needs...
            }
        };
    });