AspNet.Security.OAuth.Providers icon indicating copy to clipboard operation
AspNet.Security.OAuth.Providers copied to clipboard

TikTok authentication

Open Alex-Dobrynin opened this issue 2 years ago • 4 comments

It Would be great if you could provide TikTok auth implementation

Alex-Dobrynin avatar Mar 17 '22 12:03 Alex-Dobrynin

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

kevinchalet avatar Mar 17 '22 21:03 kevinchalet

@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.

egbakou avatar Oct 13 '23 09:10 egbakou

@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
}

Alex-Dobrynin avatar Oct 13 '23 10:10 Alex-Dobrynin

Care to contribute a PR to actually add the implementation?

martincostello avatar Oct 13 '23 11:10 martincostello