Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TikTok authentication #664

Open
Alex-Dobrynin opened this issue Mar 17, 2022 · 4 comments
Open

TikTok authentication #664

Alex-Dobrynin opened this issue Mar 17, 2022 · 4 comments

Comments

@Alex-Dobrynin
Copy link

It Would be great if you could provide TikTok auth implementation

@kevinchalet
Copy link
Member

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

@egbakou
Copy link

egbakou commented Oct 13, 2023

@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
Copy link
Author

Alex-Dobrynin commented Oct 13, 2023

@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
Copy link
Member

Care to contribute a PR to actually add the implementation?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

4 participants