AspNet.Security.OAuth.Providers
AspNet.Security.OAuth.Providers copied to clipboard
TikTok authentication
It Would be great if you could provide TikTok auth implementation
We typically rely on external contributions when it comes to adding new providers. Would you be interested?
@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 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
}
Care to contribute a PR to actually add the implementation?