aspnet-contrib / AspNet.Security.OAuth.Providers

OAuth 2.0 social authentication providers for ASP.NET Core
Apache License 2.0
2.38k stars 538 forks source link

TikTok authentication #664

Open Alex-Dobrynin opened 2 years ago

Alex-Dobrynin commented 2 years ago

It Would be great if you could provide TikTok auth implementation

kevinchalet commented 2 years ago

We typically rely on external contributions when it comes to adding new providers. Would you be interested?

egbakou commented 12 months ago

@kevinchalet @Alex-Dobrynin I'm interested in tackling this task. I've been attempting to register a demo app on the TikTok developer portal, but unfortunately, it has been rejected twice. If anyone has successfully registered an app on TikTok and is willing to share the OAuth credentials with me, I would be more than happy to proceed and submit a PR.

Alex-Dobrynin commented 12 months ago

@egbakou Register tiktok oauth:

public static AuthenticationBuilder AddTikTokAuthExtension([NotNull] this AuthenticationBuilder builder)
{
    return builder.AddOAuth<OAuthOptions, ExtendedTikTokHandler>("TikTok", t =>
    {
        t.ClientId = ConfigurationManager.AppSetting["TikTokAPISettings:TikTokAppId"];
        t.ClientSecret = ConfigurationManager.AppSetting["TikTokAPISettings:TikTokAppSecret"];
        t.CorrelationCookie.SameSite = SameSiteMode.Unspecified;

        // Add TikTok permissions
        foreach (var permission in Services.Helpers.Constants.TikTokPermissions)
        {
            t.Scope.Add(permission);
        }

        t.AuthorizationEndpoint = Constants.TikTokAuthorizationEndpointUrl;
        t.TokenEndpoint = Constants.TikTokTokenEndpointUrl;

        t.SaveTokens = true;
        t.CallbackPath = "/TikTok";

        t.Events.OnRemoteFailure = OnRemoteFailure;
    });
}

Tiktok urls and permissions (note, permissions rely on your needs):

public const string TikTokApiUrl = "https://open-api.tiktok.com/";
public const string TikTokUrl = "https://www.tiktok.com/";
public const string TikTokUserPageUrl = "https://www.tiktok.com/@";
public const string TikTokUserInfoUrl = "user/info/";
public static readonly object[] TikTokUserInfoFields = { "open_id", "union_id", "avatar_url", "avatar_url_100", "avatar_url_200", "avatar_large_url", "display_name" };
public const string TikTokUserVideosUrl = "video/list/";
public static readonly object[] TikTokUserVideosFields = { "create_time", "cover_image_url", "share_url", "video_description", "duration", "height", "width", "id", "title", "embed_html", "embed_link", "like_count", "comment_count", "share_count", "view_count" };
public const int MaxNumberOfPostsFromTikTok = 20;
public const int MaxNumberOfPostsFromTikTokForGettingUsername = 1;
public static readonly string[] TikTokPermissions = { "user.info.basic", "video.list" };
public const string TikTokRefreshAccessTokenUrl = "oauth/refresh_token/";

TikTok oauth handler:

public class ExtendedTikTokHandler : OAuthHandler<OAuthOptions>
{
    private readonly IValidator _validator;

    protected new OAuthEvents Events
    {
        get => base.Events;
        set => base.Events = value;
    }

    #region .ctor

    /// <summary>
    /// Initializes a new instance of <see cref="OAuthHandler{TOptions}"/>.
    /// </summary>
    /// <inheritdoc />
    public ExtendedTikTokHandler(IOptionsMonitor<OAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock,
        IValidator validator)
        : base(options, logger, encoder, clock)
    {
        _validator = validator;
    }

    #endregion

    #region Methods

    /// <inheritdoc />
    protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
    {
        var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, tokens.Response.RootElement);
        context.RunClaimActions();

        var deserializedJson = JsonConvert.DeserializeObject<Data>(context.TokenResponse.Response.RootElement.ToString());

        // Get the TikTok connection string (TikTok user Open Id)
        GetConnectionString(context, deserializedJson?.OpenId);

        return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
    }

    protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context)
    {
        var tokenRequestParameters = new Dictionary<string, string>
        {
            {"client_key", Options.ClientId},
            {"redirect_uri", Constants.ExtendedTikTokHandlerRedirectUri},
            {"client_secret", Options.ClientSecret},
            {"code", context.Code},
            {"grant_type", "authorization_code"}
        };

        if (context.Properties.Items.TryGetValue(OAuthConstants.CodeVerifierKey, out var codeVerifier))
        {
            tokenRequestParameters.Add(OAuthConstants.CodeVerifierKey, codeVerifier!);
            context.Properties.Items.Remove(OAuthConstants.CodeVerifierKey);
        }

        var requestContent = new FormUrlEncodedContent(tokenRequestParameters!);
        var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
        requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        requestMessage.Content = requestContent;
        requestMessage.Version = Backchannel.DefaultRequestVersion;
        var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted);

        if (response.IsSuccessStatusCode)
        {
            var contentAsString = await response.Content.ReadAsStringAsync(Context.RequestAborted);
            var deserializedJson = JsonConvert.DeserializeObject<TikTokAccessTokenResult>(contentAsString);
            var dataString = JsonConvert.SerializeObject(deserializedJson?.Data);

            var payload = JsonDocument.Parse(dataString);
            var result = OAuthTokenResponse.Success(payload);

            // If TikTok access token is not valid or user has not granted all permissions for the application
            _validator.ValidateTikTokAccessToken(deserializedJson);

            return result;
        }
        else
        {
            var error = "OAuth token endpoint failure: ";

            return OAuthTokenResponse.Failed(new Exception(error));
        }
    }

    protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
    {
        var scopeParameter = properties.GetParameter<ICollection<string>>(OAuthChallengeProperties.ScopeKey);
        var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope().Replace(" ", ",");

        var parameters = new Dictionary<string, string>
        {
            {"client_key", Options.ClientId},
            {"scope", scope},
            {"response_type", "code"},
            {"redirect_uri", Constants.ExtendedTikTokHandlerRedirectUri}
        };
        if (Options.UsePkce)
        {
            var bytes = new byte[32];
            RandomNumberGenerator.Fill(bytes);
            var codeVerifier = Microsoft.AspNetCore.WebUtilities.Base64UrlTextEncoder.Encode(bytes);
            // Store this for use during the code redemption.
            properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
            var challengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier));
            var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);
            parameters[OAuthConstants.CodeChallengeKey] = codeChallenge;
            parameters[OAuthConstants.CodeChallengeMethodKey] = OAuthConstants.CodeChallengeMethodS256;
        }
        parameters["state"] = Options.StateDataFormat.Protect(properties);

        return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters!);
    }

    // Get the TikTok connection string (TikTok user Open Id)
    private void GetConnectionString(OAuthCreatingTicketContext context, string tikTokUserOpenId)
    {
        var tokens = context.Properties.GetTokens().ToList();

        tokens.Add(new AuthenticationToken
        {
            Name = Constants.SocialConnectionString,
            Value = tikTokUserOpenId
        });

        context.Properties.StoreTokens(tokens);
    }

    #endregion
}
martincostello commented 12 months ago

Care to contribute a PR to actually add the implementation?