accessing Twitter API from Google Apps Script - twitter

I'm trying to read in a Google sheet my Twitter timeline.
I've copied the following code reported in the GAS documentation about twitter authentication (omitting step 2 since I'm not using the code inside a UI):
function getTwitterService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth1.createService('twitter')
// Set the endpoint URLs.
.setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
.setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
.setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
// Set the consumer key and secret.
.setConsumerKey('mykey')
.setConsumerSecret('mysecret')
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties());
}
function authCallback(request) {
var twitterService = getTwitterService();
var isAuthorized = twitterService.handleCallback(request);
if (isAuthorized) {
return Logger.log('Success! You can close this tab.');
} else {
return Logger.log('Denied. You can close this tab');
}
}
function makeRequest() {
var twitterService = getTwitterService();
var response = twitterService.fetch('https://api.twitter.com/1.1/statuses/user_timeline.json');
Logger.log(response);
}
but I obtain the message error: Service not authorized. (row 292, file "Service", project "OAuth1").
What's wrong?

I needed to add the following line the first time I execute makeRequest:
var authorizationUrl = twitterService.authorize();
Logger.log(authorizationUrl);
Then, open the url read from the log and authorize the app.
After that, all works fine.

Related

Difference between oauth2/v2 vs oidc Spotify API

i'm currently trying to connect via UNO-Plattform sample to the Spotify API.
https://github.com/unoplatform/Uno.Samples/blob/master/UI/Authentication.OidcDemo/Authentication.OidcDemo/Authentication.OidcDemo.Shared/MainPage.xaml.cs
Therefore I have updated the PrepareClient method.
private async void PrepareClient()
{
var redirectUri = WebAuthenticationBroker.GetCurrentApplicationCallbackUri().OriginalString;
// Create options for endpoint discovery
var options = new OidcClientOptions
{
Authority = "https://accounts.spotify.com", //"https://demo.duendesoftware.com/",
ClientId = "7c1....a45",
ClientSecret = "4b..a",
Scope = "playlist-read-private",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect,
Flow = OidcClientOptions.AuthenticationFlow.AuthorizationCode
};
// Create the client. In production application, this is often created and stored
// directly in the Application class.
_oidcClient = new OidcClient(options);
var extra_parameters = new Dictionary<string, string>();
//extra_parameters.Add("response_type", "token"); // if i add this line i get an error
_loginState = await _oidcClient.PrepareLoginAsync(extra_parameters);
btnSignin.IsEnabled = true;
// Same for logout url.
//If i add this line a get an error
//_logoutUrl = new Uri(await _oidcClient.PrepareLogoutAsync(new LogoutRequest()));
btnSignout.IsEnabled = true;
}
private async void SignIn_Clicked(object sender, RoutedEventArgs e)
{
var startUri = new Uri(_loginState.StartUrl);
// Important: there should be NO await before calling .AuthenticateAsync() - at least
// on WebAssembly, in order to prevent triggering the popup blocker mechanisms.
var userResult = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.None, startUri);
if (userResult.ResponseStatus != WebAuthenticationStatus.Success)
{
txtAuthResult.Text = "Canceled";
// Error or user cancellation
return;
}
// User authentication process completed successfully.
// Now we need to get authorization tokens from the response
var authenticationResult = await _oidcClient.ProcessResponseAsync(userResult.ResponseData, _loginState);
if (authenticationResult.IsError)
{
var errorMessage = authenticationResult.Error;
// TODO: do something with error message
txtAuthResult.Text = $"Error {errorMessage}";
return;
}
// That's completed. Here you have to token, ready to do something
var token = authenticationResult.AccessToken;
var refreshToken = authenticationResult.RefreshToken;
// TODO: make something useful with the tokens
txtAuthResult.Text = $"Success, token is {token}";
}
If i use Postman for authentication, i can use the URL
curl --location --request GET 'https://accounts.spotify.com/authorize?response_type=token&client_id=7c...45&scope=playlist-read-private&redirect_uri=http://localhost:8080&state=test'
and everything works fine and i get the token in the callback url as parameter.
If i add as "extra_parameters" the "response_type" : "token" i get the message, that this parameter is not supported...
I'm a little bit stucked here and don't know how to proceed.
I'm happy about any help in every direction to get this autentication done with uno-plattform.
OIDC can be described as a superset of OAuth2. It is a way for an identity provider to issue tokens and supply info about a user via additional APIs. Read more here.
The Oidc code that you use (probably IdentityModel.OidcClient?) requires a the service you’re calling to implement a few extra endpoints which Spotify has not implemented for their API. This is discussed in this forum topic. Because of the missing Oidc support, your code will try making calls that do not work.
The SpotifyAPI-NET library might also help you authenticate and make API calls instead.

MSAL.NET OBO refresh token problems

I am trying to implement an OBO flow through to the graph API on a middle-tier API (.NET 5.0) using MSAL.NET. I'm running into two frustrating problems, and I can't find anyone having similar problems, so I think I'm misunderstanding something!
Problem 1: Whenever I call MSAL's GetAccountAsync, it always returns null when there should be an account loaded.
Problem 2: Whenever I call MSAL's AcquireTokenSilent, I always get the error "No refresh token found in the cache." even though I got one.
Here's what I have:
Once the web app authenticates, it passes through the token to a graph auth endpoint on the API:
var authenticationResult = await ClaimHelper.ClientApplication.AcquireTokenByAuthorizationCode(GraphHelpers.BasicGraphScopes, context.Code).ExecuteAsync();
var apiUserSession = await CouncilWiseAPIHelper.APIClient.Graph.AuthoriseUserAsync(authenticationResult.AccessToken);
which seems to work fine, and passes through a JWT to the API auth endpoint. The API implements an MSAL Confidential Client application and uses the SetBeforeAccess/SetAfterAccess token cache methods to save the cache to a database.
_msalClient = ConfidentialClientApplicationBuilder.Create(_graphConfig.ClientId)
.WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs)
.WithClientSecret(_graphConfig.ClientSecret)
.Build();
SetSerialiser(serialiser);
public void SetSerialiser(MSALTokenCacheSerialiser serialiser)
{
_msalClient.UserTokenCache.SetBeforeAccessAsync(serialiser.BeforeAccessCallbackAsync);
_msalClient.UserTokenCache.SetAfterAccessAsync(serialiser.AfterAccessCallbackAsync);
}
And the serialiser methods look like this:
public async Task BeforeAccessCallbackAsync(TokenCacheNotificationArgs notification)
{
GraphUserTokenCache tokenCache = await _graphUserTokenCacheRepository.GetByUserIdentifier(notification.SuggestedCacheKey);
if (tokenCache == null)
{
tokenCache = await _graphUserTokenCacheRepository.Get(notification.SuggestedCacheKey);
}
if (tokenCache != null)
{
notification.TokenCache.DeserializeMsalV3(tokenCache.Value);
}
}
public async Task AfterAccessCallbackAsync(TokenCacheNotificationArgs notification)
{
if (!notification.HasTokens)
{
// Delete from the cache
await _graphUserTokenCacheRepository.Delete(notification.SuggestedCacheKey);
}
if (!notification.HasStateChanged)
{
return;
}
GraphUserTokenCache tokenCache;
if (notification.SuggestedCacheKey == notification.Account.HomeAccountId.Identifier)
{
tokenCache = await _graphUserTokenCacheRepository.GetByUserIdentifier(notification.SuggestedCacheKey);
}
else
{
tokenCache = await _graphUserTokenCacheRepository.Get(notification.SuggestedCacheKey);
}
if (tokenCache == null)
{
var cache = notification.TokenCache.SerializeMsalV3();
tokenCache = new GraphUserTokenCache
{
Id = notification.SuggestedCacheKey,
AccountIdentifier = notification.Account.HomeAccountId.ToString(),
Value = cache
};
await _graphUserTokenCacheRepository.Add(tokenCache);
}
else
{
await _graphUserTokenCacheRepository.Update(tokenCache.Id, notification.TokenCache.SerializeMsalV3());
}
}
I can see the token BeforeAccess and AfterAccess methods being called, and I can see the caches being created in the database (encryption has been removed while I'm trying to track down this issue). If I inspect the serialised token cache being saved, it NEVER has a refresh token populated, but if I inspect the requests with fiddler I can see a refresh token was indeed provided.
Finally, here is the code for retrieving the access token which is called whenever a graph request is made:
public async Task<AuthenticationResult> GetAccessToken(string accountId, string jwtBearerToken)
{
try
{
IAccount account = null;
if (accountId.IsNotNullOrEmpty())
{
account = await _msalClient.GetAccountAsync(accountId);
}
var scope = _graphConfig.Scopes.Split(' ');
if (account == null)
{
var result = await _msalClient.AcquireTokenOnBehalfOf(scope,
new UserAssertion(jwtBearerToken))
.ExecuteAsync();
return result;
}
else
{
var result = await _msalClient.AcquireTokenSilent(scope, account)
.ExecuteAsync();
return result;
}
}
catch (MsalClientException ex)
{
ex.CwApiLog();
return null;
}
catch(Exception ex)
{
ex.CwApiLog();
return null;
}
}
When it's called with the jwtBearerToken, it will successfully call AcquireTokenOnBehalfOf() and the token is cached and a result returned, but when I come back to retrieve the account via GetAccountAsync() it always returns null even though I can see the token cache was loaded in BeforeAccessCallbackAsync().
Also, even if I call AcquireTokenSilent() immediately after acquiring the obo token with the account it just returned, I will get an exception saying there is no refresh token in the cache.
I am totally lost on what I'm doing wrong here, any help would be greatly appreciated.
I recently ran into the same problem while running a long runing OBO flow, MSAL has recently implemented an interface ILongRunningWebApi for these use cases you can go and see this new documentation
Here is an extract:
One OBO scenario is when a web API runs long running processes on
behalf of the user (for example, OneDrive which creates albums for
you). Starting with MSAL.NET 4.38.0, this can be implemented as such:
Before you start a long running process, call:
string sessionKey = // custom key or null
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
.InitiateLongRunningProcessInWebApi(
scopes,
userToken,
ref sessionKey)
.ExecuteAsync();
userToken is a user token used to call this web API. sessionKey will
be used as a key when caching and retrieving the OBO token. If set to
null, MSAL will set it to the assertion hash of the passed-in user
token. It can also be set by the developer to something that
identifies a specific user session, like the optional sid claim from
the user token (for more information, see Provide optional claims to
your app). If the cache already contains a valid OBO token with this
sessionKey, InitiateLongRunningProcessInWebApi will return it.
Otherwise, the user token will be used to acquire a new OBO token from
AAD, which will then be cached and returned.
In the long-running process, whenever OBO token is needed, call:
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
.AcquireTokenInLongRunningProcess(
scopes,
sessionKey)
.ExecuteAsync();
Pass the sessionKey which is associated with the current user's
session and will be used to retrieve the related OBO token. If the
token is expired, MSAL will use the cached refresh token to acquire a
new OBO access token from AAD and cache it. If no token is found with
this sessionKey, MSAL will throw a MsalClientException. Make sure to
call InitiateLongRunningProcessInWebApi first.
Hope this helps :)

User is always null when using AspNet.Security.OpenIdConnect.Server

I'm trying to generate access tokens for my aspnet core web app. I created the following provider:
public class CustomOpenIdConnectServerProvider : OpenIdConnectServerProvider
{
public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
{
// Reject the token requests that don't use grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() && !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only the resource owner password credentials and refresh token " +
"grants are accepted by this authorization server");
return Task.FromResult(0);
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private), call Skip()
// to inform the server the request should be accepted without
// enforcing client authentication.
context.Skip();
return Task.FromResult(0);
}
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
// Resolve ASP.NET Core Identity's user manager from the DI container.
var manager = context.HttpContext.RequestServices.GetRequiredService<UserManager<User>>();
// Only handle grant_type=password requests and let ASOS
// process grant_type=refresh_token requests automatically.
if (context.Request.IsPasswordGrantType())
{
var user = await manager.FindByNameAsync(context.Request.Username);
if (user == null)
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid credentials.");
return;
}
// Ensure the password is valid.
if (!await manager.CheckPasswordAsync(user, context.Request.Password))
{
if (manager.SupportsUserLockout)
{
await manager.AccessFailedAsync(user);
}
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid credentials.");
return;
}
if (manager.SupportsUserLockout)
{
await manager.ResetAccessFailedCountAsync(user);
}
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
// Note: the name identifier is always included in both identity and
// access tokens, even if an explicit destination is not specified.
identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserIdAsync(user));
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, await manager.GetUserIdAsync(user));
// When adding custom claims, you MUST specify one or more destinations.
// Read "part 7" for more information about custom claims and scopes.
identity.AddClaim("username", await manager.GetUserNameAsync(user),
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var claims = await manager.GetClaimsAsync(user);
foreach (var claim in claims)
{
identity.AddClaim(claim.Type, claim.Value, OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Set the list of scopes granted to the client application.
ticket.SetScopes(
/* openid: */ OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.OfflineAccess,
/* email: */ OpenIdConnectConstants.Scopes.Email,
/* profile: */ OpenIdConnectConstants.Scopes.Profile);
// Set the resource servers the access token should be issued for.
ticket.SetResources("resource_server");
context.Validate(ticket);
}
}
This works just fine, I can get the access token and the users are authenticated successfully. The issue that I'm facing here is that in any authorized action method when I do this: var user = await _userManager.GetUserAsync(User); the value for user is always null! Of course, I'm passing the Authorization header with a valid access token and the request goes into actions annotated with Authorize without any problems. It's just the value of user is null. Can anybody tell me whats wrong with my code?
By default, UserManager.GetUserAsync(User) uses the ClaimTypes.NameIdentifier claim as the user identifier.
In your case, ClaimTypes.NameIdentifier - which is no longer considered by the OpenID Connect server middleware as a special claim in 1.0 - is not added to the access token because it doesn't have the appropriate destination. As a consequence, Identity is unable to extract the user identifier from the access token.
You have 3 options to fix that:
Replace the default user identifier claim used by Identity by calling services.Configure<IdentityOptions>(options => options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject); in your Startup.ConfigureServices() method.
Keep using the ClaimTypes.NameIdentifier claim but give it the right destination (OpenIdConnectConstants.Destinations.AccessToken).
Use UserManager.FindByIdAsync(User.FindFirstValue(OpenIdConnectConstants.Claims.Subject)) instead of UserManager.GetUserAsync(User).

Issues with OAuth 2.0 Library for Google Apps Scripts for MEETUP

I am using OAuth 2.0 Library for Google Spreadsheet for Meetup Api
Code.gs
function getMeetupService() {
return OAuth2.createService('meetup')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://secure.meetup.com/oauth2/authorize')
.setTokenUrl('https://secure.meetup.com/oauth2/access')
// Set the client ID and secret, from the Google Developers Console.
.setClientId('.....')
.setClientSecret('......')
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
.setTokenFormat(OAuth2.TOKEN_FORMAT.FORM_URL_ENCODED);
}
_eventId = null;
function startService(eventId) {
var meetupService = getMeetupService();
if (!meetupService.hasAccess()) {
var authorizationUrl = meetupService.getAuthorizationUrl();
return authorizationUrl;
} else {
//Yet to decide
}
return 123;
}
function authCallback(request) {
var meetupService = getMeetupService();
var isAuthorized = meetupService.handleCallback(request);
if (isAuthorized) {
//getAllDetails(_eventId);
} else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
Sidebar.html
$(function() {
checkService();
});
function checkService() {
google.script.run
.withSuccessHandler(function(url) {
if(url) {
$('body').append('Click here to Login into Meetup');
$('#run-get-details').attr("onclick", 'window.open("' + url + '")');
} else {
$('#run-get-details').attr("url", '');
}
$('#run-get-details').prop("disabled", false);
})
.withFailureHandler(failureHandler)
.startService($("#eventId").val());
}
function authCallback(request) {
alert(123);
}
Everything seems to be working till the link. Once the link is clicked it opens the authentication window of the meetup exactly i want. But then once I authorize the app in meetup and when it gets redirected to script.google.com
https://script.google.com/a/macros/{domain}/d/{PROJECT_ID}/usercallback?code={code}&state=ADEpC8wvspQd-+VXyQ0mOhYNenFu9Ib08hK6xfJ2gNJwcTxrIB4nVfphDyhejUHEMdgD0OhtwdcEs46KdhUZMD-Ekwftj3bzXwdi-mKc8PLKd7SsAYYqE-ZVZD2tm1HmyxtKYJCkoeg7R1K5DIMedYp38BiJ4F2Hbtei34fEdveObQhMSMDt1f2ufrmDzGh5W+fxdXMEmrfeCINO23hC8yV0JRXVDkErL3t8pQih8DicQoY6k2uqHThK8BqfSioBkgPZ0SPvj3Krtpgj9R+XWGtPqRRAwPum4k8etMROvy2DLT1ENNJdrVw
But then its throwing the error
The state token is invalid or has expired. Please try again.
Someone please help me in this.
So looking into this a bit further I found the issue. The Meetup server is changing the state token. If you copy your token from getAuthorizationUrl() and paste it into the state parameter in the url of the bad callback the Auth flow will successfully continue.
State Token made by Apps Script
ADEpC8zI8-E38GrIi2sB5Pf1xK2Hn-6XQ-SJLa0gmxos6z-hbvedTJ2UvXzSJxXbE_NfJlpHkOsjN4DLJ0sOGsYIZpTBc3OpAMWuoj-8UjUuKcTs1htZknyzv0QX9pPe-McxB_MC1fbGBjvrwEGP5_58tQdfRf3K70LURbe0cZ2qx_YK5xxN2qE
Returned State Token
ADEpC8zI8-E38GrIi2sB5Pf1xK2Hn-6XQ-SJLa0gmxos6z-hbvedTJ2UvXzSJxXbE+NfJlpHkOsjN4DLJ0sOGsYIZpTBc3OpAMWuoj-8UjUuKcTs1htZknyzv0QX9pPe-McxB+MC1fbGBjvrwEGP5+58tQdfRf3K70LURbe0cZ2qx+YK5xxN2qE

logging in with different user / resource owner

i am trying to write a tool that creates entries in the google calendar.
after following the google docs and creating an client-identifier/secret in the api console, i managed to put together a client that authenticates correctly and shows my registered google calendars. right now for me it looks like my google-account is somehow tied to my client-identifier/secret. what i want to know is: how can i change the auth process so that it is possible for an other user of this tool to enter his google-id and get access to his calendars?
EDIT: in other words (used in the RFC): I want make the resource owner-part editable while leaving the client-part unchanged. but my example, although working, ties together client and resource owner.
here is my app that works fine so far:
public void Connect()
{
var provider = new NativeApplicationClient(GoogleAuthenticationServer.Description);
provider.ClientIdentifier = "123456123456.apps.googleusercontent.com";
provider.ClientSecret = "nASdjKlhnaxEkasDhhdfLklr";
var auth = new OAuth2Authenticator<NativeApplicationClient>(provider, GetAuthorization);
var service = new CalendarService(auth);
//Events instances = service.Events.Instances("primary", "recurringEventId").Fetch();
var list = service.CalendarList.List().Fetch();
foreach (var itm in list.Items)
Console.WriteLine(itm.Summary);
}
private static readonly byte[] AditionalEntropy = { 1, 2, 3, 4, 5 };
private static IAuthorizationState GetAuthorization(NativeApplicationClient arg)
{
var state = new AuthorizationState(new[] { CalendarService.Scopes.Calendar.GetStringValue() });
state.Callback = new Uri(NativeApplicationClient.OutOfBandCallbackUrl);
var refreshToken = LoadRefreshToken();
if (!String.IsNullOrWhiteSpace(refreshToken))
{
state.RefreshToken = refreshToken;
if (arg.RefreshToken(state))
return state;
}
var authUri = arg.RequestUserAuthorization(state);
// Request authorization from the user (by opening a browser window):
Process.Start(authUri.ToString());
var frm = new FormAuthCodeInput();
frm.ShowDialog();
// Retrieve the access token by using the authorization code:
var auth = arg.ProcessUserAuthorization(frm.txtAuthCode.Text, state);
StoreRefreshToken(state);
return auth;
}
private static string LoadRefreshToken()
{
try
{
return Encoding.Unicode.GetString(ProtectedData.Unprotect(Convert.FromBase64String(Properties.Settings.Default.RefreshToken), AditionalEntropy, DataProtectionScope.CurrentUser));
}
catch
{
return null;
}
}
private static void StoreRefreshToken(IAuthorizationState state)
{
Properties.Settings.Default.RefreshToken = Convert.ToBase64String(ProtectedData.Protect(Encoding.Unicode.GetBytes(state.RefreshToken), AditionalEntropy, DataProtectionScope.CurrentUser));
Properties.Settings.Default.Save();
}
Prompt the user to enter their ClientIdentifier and ClientSecret, then pass these values to your Connect method.
i solved the problem myself.
the problem was, that i'm usually always connected to google and because i did't log out from google before my app redirected to google to get the access-token, google automatically generated the access-token for my account - skipping the part where an input-form appears where anyone could enter his/her user-credentials to let google generate an access-token for his/her account.

Resources