Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to setup Social Login in ASP.NET Boilerplate?

I'm trying to authenticate users through Google. I'm using the ABP startup template for ASP.NET Core with Vue.

Here's what I have done so far:

I've created a GoogleAuthProviderApi in Web.Core:

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Google;
using Newtonsoft.Json.Linq;

namespace Mindbus.MindbooksSEO.Authentication.External.Google
{
    public class GoogleAuthProviderApi : ExternalAuthProviderApiBase
    {
        public const string Name = "Google";

        public override async Task<ExternalAuthUserInfo> GetUserInfo(string accessCode)
        {
            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OAuth middleware");
                client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
                client.Timeout = TimeSpan.FromSeconds(30);
                client.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB

                var request = new HttpRequestMessage(HttpMethod.Get, GoogleDefaults.UserInformationEndpoint);
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessCode);

                var response = await client.SendAsync(request);

                response.EnsureSuccessStatusCode();

                var payload = JObject.Parse(await response.Content.ReadAsStringAsync());

                return new ExternalAuthUserInfo
                {
                    //Name = GoogleHelper.GetName(payload),
                    EmailAddress = GoogleHelper.GetEmail(payload),
                    //Surname = GoogleHelper.GetFamilyName(payload),
                    //ProviderKey = GoogleHelper.GetId(payload),
                    Provider = Name
                };
            }
        }
    }
}

I've registered the Google External Authentication in AuthConfigurer.cs in Web.Host:

if (bool.Parse(configuration["Authentication:Google:IsEnabled"]))
{
    services.AddAuthentication().AddGoogle(googleOptions =>
    {
        googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
        googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];
    });
}

I've added the settings to appsettings.json in Web.Host, and created the corresponding secrets (ClientId and ClientSecret) in the Secret Manager tool.

I've forced the API to SSL with the RequireHttpsAttribute.

I've registered the GoogleAuthProviderApi in the [ProjectName]WebCoreModule.cs:

public override void PreInitialize()
{
    Configuration.DefaultNameOrConnectionString = _appConfiguration.GetConnectionString(
        MindbooksSEOConsts.ConnectionStringName
    );

    // Use database for language management
    Configuration.Modules.Zero().LanguageManagement.EnableDbLocalization();

    Configuration.Modules.AbpAspNetCore()
            .CreateControllersForAppServices(
                typeof(MindbooksSEOApplicationModule).GetAssembly()
            );

    ConfigureTokenAuth();

    Configuration.Modules.Zero().UserManagement.ExternalAuthenticationSources.Add<GoogleAuthProviderApi>();
}

I don't know what I'm missing here, nor do I know what to expect exactly.

I would have thought that a call to the api/TokenAuth/GetExternalAuthenticationProviders endpoint would at least give me a list with Google in it, but this request returns with an empty array in the results.

Additionally, it's a bit unclear to me what the scope of this external authentication is, in the case of OAuth providers such as Google and Facebook. It seems to me that you either have OAuth for server-side usage, in which case I don't understand why you would expose part of it through the API. Or you have OAuth for JavaScript web apps, in which case you don't need an API endpoint on your own server, you just handle the entire thing client-side through the web app.

So, what's the purpose exactly of the External Authenticate API endpoint? Is it so that your own server acts as a proxy for authentication? So that you can have both client-side and server-side usage of the External (Google) API?

Update 1

The comments require me to add some clarification.

#1: If I add the Abp.TenantId header in Postman, the response remains the same:

GET /api/TokenAuth/GetExternalAuthenticationProviders HTTP/1.1
Host: localhost:44300
Accept: application/json
Abp.TenantId: 2
Cache-Control: no-cache
Postman-Token: 0cb72e57-4b9a-474d-b60d-492fa727a7a2

#2: The console "tricks" in Swagger result in errors:

abp.swagger.login()
undefined
VM40:49 POST https://localhost:44300/api/TokenAuth/Authenticate 500 ()
abp.swagger.login @ VM40:49
(anonymous) @ VM84:1
abp.swagger.addAuthToken()
false

Update 2

I think there's something wrong with the GoogleAuthProviderApi. After I made the debugger break on all CLR Exceptions, I caught the following error:

'Mindbus.MindbooksSEO.Authentication.External.Google.GoogleAuthProviderApi' to type
 'Abp.Authorization.Users.IExternalAuthenticationSource`2
[Mindbus.MindbooksSEO.MultiTenancy.Tenant,
Mindbus.MindbooksSEO.Authorization.Users.User]'.'
like image 808
gijswijs Avatar asked Apr 13 '18 01:04

gijswijs


2 Answers

ASP.NET Core 1.x or MVC 5

  1. Note that the configuration for Social Login providers (e.g. Google) is quite different from External Authentication sources (e.g. LDAP). So, remove this line:

    Configuration.Modules.Zero().UserManagement.ExternalAuthenticationSources.Add<GoogleAuthProviderApi>();

  2. Observe that GetExternalAuthenticationProviders looks in IExternalAuthConfiguration.
    So, configure IExternalAuthConfiguration in PostInitialize method of *WebHostModule:

    if (bool.Parse(configuration["Authentication:Google:IsEnabled"]))
    {
        var externalAuthConfiguration = IocManager.Resolve<IExternalAuthConfiguration>();
    
        externalAuthConfiguration.Providers.Add(
            new ExternalLoginProviderInfo(
                GoogleAuthProviderApi.Name,
                configuration["Authentication:Google:ClientId"],
                configuration["Authentication:Google:ClientSecret"],
                typeof(GoogleAuthProviderApi)
            )
        );
    }
    

ASP.NET Core 2.x

Although the above way of handling Social Login providers may work, it is no longer recommended.

The built-in .AddGoogle way:

if (bool.Parse(configuration["Authentication:Google:IsEnabled"]))
{
    services.AddAuthentication().AddGoogle(googleOptions =>
    {
        googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
        googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];
    });
}

...is meant to be used with:

var result = await _signInManager.ExternalLoginSignInAsync(
    info.LoginProvider,
    info.ProviderKey,
    isPersistent: false,
    bypassTwoFactor : true
);

And the way to get external authentication schemes is:

var schemes = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

You can modify GetExternalAuthenticationProviders to return this data.

like image 63
aaron Avatar answered Nov 16 '22 11:11

aaron


For those guys who visit this topic and don't find an answer (as me).

aspnetcore 2.2 and abp 4.5.0 Facebook working example

*WebHostModule.cs

public override void PostInitialize()
        {
            var externalAuthConfiguration = IocManager.Resolve<IExternalAuthConfiguration>();
            externalAuthConfiguration.Providers.Add(
                 new ExternalLoginProviderInfo(
                    FacebookAuthProvider.Name,
                    configuration["Authentication:Facebook:ClientId"],
                    configuration["Authentication:Facebook:Secret"],
                    typeof(FacebookAuthProvider)
                )
            );           
        }

*FacebookAuthProvider.cs

public class FacebookAuthProvider: ExternalAuthProviderApiBase
    {
        private static readonly HttpClient Client = new HttpClient();
        public const string Name = "Facebook";
        public override async Task<ExternalAuthUserInfo> GetUserInfo(string accessCode)
        {
            //gen app access token
            var appAccessTokenResponse = await Client.GetStringAsync("https://graph.facebook.com/oauth/access_token" +
              "?client_id=" + ProviderInfo.ClientId +
              "&client_secret=" + ProviderInfo.ClientSecret +
              "&grant_type=client_credentials");
            var appAccessToken = JsonConvert.DeserializeObject<FacebookAppAccessToken>(appAccessTokenResponse);
            //validate user access token
            var userAccessTokenValidationResponse = await Client.GetStringAsync("https://graph.facebook.com/v3.2/debug_token" +
                "?input_token="+ accessCode +
                "&access_token="+ appAccessToken.AccessToken);
            var userAccessTokenValidation = JsonConvert.DeserializeObject<FacebookUserAccessTokenValidation>(userAccessTokenValidationResponse);
            if (!userAccessTokenValidation.Data.IsValid)
            {
                throw new ArgumentException("login_failure Invalid facebook token.");
            }

            //get userinfo
            var userInfoResponse = await Client.GetStringAsync($"https://graph.facebook.com/v3.2/me?fields=id,email,first_name,last_name&access_token={accessCode}");
            var userInfo = JsonConvert.DeserializeObject<FacebookUserData>(userInfoResponse);

            return new ExternalAuthUserInfo
            {
                Name = userInfo.FirstName,
                EmailAddress = userInfo.Email,
                Surname=userInfo.LastName,
                Provider=Name,
                ProviderKey=userInfo.Id.ToString()
            };

        }
    }

Models

internal class FacebookUserData
    {
        public long Id { get; set; }
        public string Email { get; set; }
        public string Name { get; set; }
        [JsonProperty("first_name")]
        public string FirstName { get; set; }
        [JsonProperty("last_name")]
        public string LastName { get; set; }
        public string Gender { get; set; }
        public string Locale { get; set; }
        public FacebookPictureData Picture { get; set; }
    }

    internal class FacebookPictureData
    {
        public FacebookPicture Data { get; set; }
    }

    internal class FacebookPicture
    {
        public int Height { get; set; }
        public int Width { get; set; }
        [JsonProperty("is_silhouette")]
        public bool IsSilhouette { get; set; }
        public string Url { get; set; }
    }

    internal class FacebookUserAccessTokenData
    {
        [JsonProperty("app_id")]
        public long AppId { get; set; }
        public string Type { get; set; }
        public string Application { get; set; }
        [JsonProperty("expires_at")]
        public long ExpiresAt { get; set; }
        [JsonProperty("is_valid")]
        public bool IsValid { get; set; }
        [JsonProperty("user_id")]
        public long UserId { get; set; }
    }

    internal class FacebookUserAccessTokenValidation
    {
        public FacebookUserAccessTokenData Data { get; set; }
    }

    internal class FacebookAppAccessToken
    {
        [JsonProperty("token_type")]
        public string TokenType { get; set; }
        [JsonProperty("access_token")]
        public string AccessToken { get; set; }
    }
like image 2
crunchysea65 Avatar answered Nov 16 '22 11:11

crunchysea65