@lucuma I can also reproduce this.
It seems like this deprecation by twitter has broken their OAuth 1.0 integration with some small changes, which is worsened by their documentation. Specifically this part.
Terminology clarification
In the guide below, you may see different terms referring to the same thing.
...
Temporary credentials:
Request Token === oauth_token
Request Token Secret === oauth_token_secret
oauth_verifier
It seems the previous implementation they populated both the RequestTokenSecret and the oauth_token_secret
but they now only populate the oauth_token_secret
. The OAuthUtils.AcquireAccessToken
method only needs the oauth_token
and oauth_verifier
(which are now the only things passed back) to then successfully fetch the temporary access token.
I was seeing an infinite loop due to the following if
check in the current implementation.
Another issue with the current implementation is the use of session.ReferrerUrl
which in my testing I found was populated with api.twitter.com
when in actual fact it is expected to return to your own domain. Current flow is either expects persisting session.ReferrerUrl
from previous request and then falls back to original Request
which on the redirect from Twitter is api.twitter.com
. This could be a problem with my test setup though, not 100% sure.
Created a minimal reproduction on GitHub here with my test app keys for ease of use.
In my testing, I created a new OAuthProvider inheriting from TwitterOAuth
and provided an override for AuthenticateAsync
with just these two changes. More testing will be needed to make sure both can ‘sunset’ apps and updated OAuth 1.0 apps can be supported side by side or if a separate OAuth provider is required.
Here is my override with small changes above and Twitter App settings for context.
public class TwitterUpdateOAuthProvider : TwitterAuthProvider
{
public TwitterUpdateOAuthProvider(IAppSettings appSettings) : base(appSettings)
{
}
public override async Task<object> AuthenticateAsync(IServiceBase authService, IAuthSession session, Authenticate request,
CancellationToken token = new CancellationToken())
{
var tokens = Init(authService, ref session, request);
var ctx = CreateAuthContext(authService, session, tokens);
//Transferring AccessToken/Secret from Mobile/Desktop App to Server
if (request.AccessToken != null && request.AccessTokenSecret != null)
{
tokens.AccessToken = request.AccessToken;
tokens.AccessTokenSecret = request.AccessTokenSecret;
var validToken = await AuthHttpGateway.VerifyTwitterAccessTokenAsync(
ConsumerKey, ConsumerSecret,
tokens.AccessToken, tokens.AccessTokenSecret, token).ConfigAwait();
if (validToken == null)
return HttpError.Unauthorized("AccessToken is invalid");
if (!string.IsNullOrEmpty(request.UserName) && validToken.UserId != request.UserName)
return HttpError.Unauthorized("AccessToken does not match UserId: " + request.UserName);
tokens.UserId = validToken.UserId;
session.IsAuthenticated = true;
var failedResult = await OnAuthenticatedAsync(authService, session, tokens, new Dictionary<string, string>(), token).ConfigAwait();
var isHtml = authService.Request.IsHtml();
if (failedResult != null)
return ConvertToClientError(failedResult, isHtml);
return isHtml
? await authService.Redirect(SuccessRedirectUrlFilter(ctx, RedirectUrl.SetParam("s", "1"))).SuccessAuthResultAsync(authService,session).ConfigAwait()
: null; //return default AuthenticateResponse
}
//Default OAuth logic based on Twitter's OAuth workflow
if ((!tokens.RequestTokenSecret.IsNullOrEmpty() && !request.oauth_token.IsNullOrEmpty()) ||
(!request.oauth_token.IsNullOrEmpty() && !request.oauth_verifier.IsNullOrEmpty()))
{
if (OAuthUtils.AcquireAccessToken(tokens.RequestTokenSecret, request.oauth_token, request.oauth_verifier))
{
session.IsAuthenticated = true;
tokens.AccessToken = OAuthUtils.AccessToken;
tokens.AccessTokenSecret = OAuthUtils.AccessTokenSecret;
//Haz Access
return await OnAuthenticatedAsync(authService, session, tokens, OAuthUtils.AuthInfo, token).ConfigAwait()
?? await authService.Redirect(SuccessRedirectUrlFilter(ctx, this.RedirectUrl.SetParam("s", "1"))).SuccessAuthResultAsync(authService,session).ConfigAwait();
}
//No Joy :(
tokens.RequestToken = null;
tokens.RequestTokenSecret = null;
await this.SaveSessionAsync(authService, session, SessionExpiry, token).ConfigAwait();
return authService.Redirect(FailedRedirectUrlFilter(ctx, RedirectUrl.SetParam("f", "AccessTokenFailed")));
}
if (OAuthUtils.AcquireRequestToken())
{
tokens.RequestToken = OAuthUtils.RequestToken;
tokens.RequestTokenSecret = OAuthUtils.RequestTokenSecret;
await this.SaveSessionAsync(authService, session, SessionExpiry, token).ConfigAwait();
//Redirect to OAuth provider to approve access
return authService.Redirect(AccessTokenUrlFilter(ctx, this.AuthorizeUrl
.AddQueryParam("oauth_token", tokens.RequestToken)
.AddQueryParam("oauth_callback", session.ReferrerUrl)
.AddQueryParam(Keywords.State, session.Id) // doesn't support state param atm, but it's here when it does
));
}
return authService.Redirect(FailedRedirectUrlFilter(ctx, session.ReferrerUrl.SetParam("f", "RequestTokenFailed")));
}
}
Usage:
appHost.Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(appSettings), /* Sign In with Username / Password credentials */
new TwitterUpdateOAuthProvider(appSettings)
{
ConsumerKey = "key",
ConsumerSecret = "secret",
RedirectUrl = "https://localhost:5001/",
CallbackUrl = "https://localhost:5001/auth/twitter"
}
}));