2017년 8월 14일 월요일

OAuth 2.0 400 - error:invalid_grant and ideas?


Apologies if I'm asking a dumb question, did search around and I struggled to find anything that matched. Please feel free to tell me to go search harder if I'm just being dumb....

I have a server app that needs to collect it's own analytics data from the API, and had planned to use the service-accounts scenario to collect the data... At first I tried to use the google libraries, but they seem to come with a world of dependency hell that I'd like to avoid if at all possible. Aside from that, I thought it would be good to try and understand the process from the ground up. 

I've had some limited success getting the JWT request together and signing it.. I *think* everything is as it should be, but when I make my first call the server always returns 400 : error:invalid_grant

I've tried generating a new key, but nothing seems to help, and I'm a bit stuck to know whats going wrong and why.

I've attached a .groovy test that demonstrates the problem if anyone is sufficiently motivated to take a look. To run it, you'll need to generate a new service account from the api console, download the key and then update the test file with your client_id and path to the downloaded .p12 file.

The test script should grab all the dependencies it needs. Hopefully someone out there will take a look and spot that I've just done something really dumb.


--
Typically...

After fighting with this for 14 odd hours, and finally posting here...
I go and fix it in 3 mins!

I thought it was just worth trying the email address from the API
console instead of the client-Id.. Hey presto, 200OK.

Hopefully this will help someone else. Google friends, can I make a
suggestion that https://developers.google.com/accounts/docs/OAuth2ServiceAccount
could use a small edit in under the "Forming the JWT Claim Set"
heading, "Required Claims" section. Specifically, the table listing
the parameters reads "iss - the client_id of the application making
the access token request". I think it should really read "the Email
address as taken from the API Console service account setting".. I'm
pretty sure it says to send client_id in a few other places, but that
page seems to be the authoritative one, and is linked from the API
console. Making it a bit clearer would have saved me a bit of pain.

--
Soooo funny.  I just spent the last two days on the same exact problem.  Yeah, very frustrating.

However, now all I get back is a 403 forbidden from the actual analytics service request. 

--
heh looking forward to that pain then :)

Please drop me a line if you sort it!

--
Yep.. same boat here and stuck again.. only getting 403 responses

--
I suppose it's not just me then.  *please* send me a note if you figure it out - I will do the same.

Also, I would be happy to use the "simple api access" method if I could find some docs on how to use that.  Are you familiar with that approach?

--
Can you post the detailed error response that you are getting ? 403
forbidden generally means that you don't have access to the profile
that you're requesting the data for. 

--
Here is the response body that I get back:


"{"error":{"errors":[{"message":"Forbidden"}],"code":403,"message":"Forbidden"}}"

* Successful OAuth2 token request - I get back an http status 200 with <access_token>
  -- with http header:  "Authorization:  Bearer <access_token>"
* Receive a 403 forbidden

I can look at the GA reports from the web interface for this profile and I believe I am an admin for this profile.

Also, I have run through the google php api client and I get the same response back using that.

Any ideas?

--
Just received a note from Nick - check his reply in my thread.

Bottom line from him is that the analytics service does not yet support OAuth "Server to Server" interactions.  He pointed towards a blog post which lists the services which support this type of interaction


Also note the suggestion in his response. 

--
I was able to get it all working.  First, you have to abandon any idea of getting a "server to server" authentication setup working against the analytics service - it doesn't support it (yet).  And, yes, it's very frustrating because everything I read lead me to believe that we should be using that - at least for my situation.  And, *nothing* I read ever gave me a sign that perhaps the analytics service did not support this authentication model yet.  <sigh>

Ok, so here is how I did it:

⦁ Go to Google APIs Console and make sure you have a "Client ID for web applications"
⦁ If you don't have that type of client ID, click "Create another Client ID..." button and create one
⦁ Go to OAuth 2.0 Playground  https://code.google.com/oauthplayground/
⦁ Click on the settings button in the top-right.
⦁ Click on "Use your own OAuth credentials"
⦁ Enter your OAuth client ID and client secret and close it
⦁ Go to step 1 and select "Analytics" and click "Authorize APIs"
⦁ Go through consent page
⦁ Go to step 2 and click "Exchange authorization code for tokens"
⦁ You should get an access token and a refresh token
⦁ Save this refresh token to a secure file which your groovy code can read 
⦁ Click on the "Refresh access token" button
⦁ Now you should see the POST on the right which gets a new access token using a refresh token
⦁ Write your groovy code which can perform that POST using the refresh token you stored in the file
⦁ You will get back an access code which you can use to perform queries against the analytics API
⦁ When it expires, you will need to obtain a new access code using the same technique

Here is the downside to this approach - first, this refresh token, and hence the queries, are now tied to the actual google user who gave the consent in the OAuth 2.0 playground.  secondly, I'm not sure about this, but I *think* that if you authorized again and got a new refresh token, the original refresh token might not work.  I would need to test this to be sure.

This worked for me and I hope it works for you. If / when the 'server to server' authentication is turned for analytics, I will switch over to that.

Let me know how it goes.

--
We're looking into supporting this.

In the mean time can you help me understand the use case on why you need 'server to server' authentication?

It seems like if you have GA today, you must have signed our TOS, and used our UI once. So how is going through the server side OAuth 2 flow once, (and storing / using refresh) tokens a less appealing option to access data though our API?

--
Hiya Nick, thanks for asking...

To be (brutally) honest, I think I've not explained myself very well..

the data API is completely fine for me.. it's just that the path I had taken through the documentation paints a very clear picture that older authentication mechanisms will be deprecated (In the very near future is the impression given). As this was starting out a new project, it seemed to only make sense to code it to the suggested api rather than the legacy one. If the old api isn't going away, I'll very happily re-code to use that. The docs certainly didn't make it obvious that OAuth2 isn't yet supported.

FWIW tho, I strongly believe that the documentation path devs are led through (from the sign-up / API screens) give the impression that OAuth2 is the only legitimate choice. So I'd suggest it's more a documentation bug than a feature request really.

Cheers, for following up, and many thanks to everyone else who has posted on this. I'll be having a go this weekend and will report back, but looks like it's sorted for now!

--
Wonderful!

thanks so much for this, you've saved me!

--
fixing docs is easy enough.

OAuth2 is both currently supported and our best practice to handle authorization.

Though notice there are 6 OAuth 2 flows documented: https://developers.google.com/accounts/docs/OAuth2

My question is why you need the Service Account flow vs the Web Server flow?

Web Server works fine today. Service Account is quite new and we need to add support in our API for it.

--
In essence, I have a server timer job that's kicked off once every 24 hours that I'd like to call into the api and get the stats from the previous day. Hence there's no browser involvement (There won't even be an attending user).. Whilst pasting the token from a previous browser session works around this, it just doesn't feel like the "right" flow to use for this case?

--
np, happy to hear it helped 

--
I have a similar use case to Ian.  We have a server job which runs periodically to get data for internal reports.

--
I have the same use case.. need to pull stats periodically from GA and
process them automatically on my server.  I didn't want to use the old
api because it will be deprecated eventually and this is a new project
so I want to make sure it is good for some time to come.  What is the
best way to stay updated on the status of GA s2s oauth2 support? 

--
I am facing 400 code as well... this is the response content...
apiHttpRequest Object
(
    [batchHeaders:apiHttpRequest:private] => Array
        (
            [Content-Type] => application/http
            [Content-Transfer-Encoding] => binary
            [MIME-Version] => 1.0
            [Content-Length] => 
        )

    [url:protected] => https://accounts.google.com/o/oauth2/token
    [requestMethod:protected] => POST
    [requestHeaders:protected] => Array
        (
            [content-type] => application/x-www-form-urlencoded
            [content-length] => 534
        )

    [postBody:protected] => grant_type=assertion&assertion_type=http%3A%2F%2Foauth.net%2Fgrant_type%2Fjwt%2F1.0%2Fbearer&assertion=quitealongstringtopaste
    [userAgent:protected] => Google Oauth2 Sample google-api-php-client/0.5.0
    [responseHttpCode:protected] => 400
    [responseHeaders:protected] => Array
        (
            [cache-control] => no-cache, no-store, max-age=0, must-revalidate
            [pragma] => no-cache
            [expires] => Fri, 01 Jan 1990 00:00:00 GMT
            [date] => Sun, 27 May 2012 21:52:03 GMT
            [content-type] => application/json
            [x-content-type-options] => nosniff
            [x-frame-options] => SAMEORIGIN
            [x-xss-protection] => 1; mode=block
            [server] => GSE
            [transfer-encoding] => chunked
        )

    [responseBody:protected] => {
  "error" : "invalid_grant"
}
    [accessKey] => 

my script uses the google-api-php-client lib provided by google, and is quite simple...

require_once("../_includes/config/header.php"); //contains the definitions of the following constants
require_once (G_API_DIR.'/src/apiClient.php');
require_once (G_API_DIR.'/src/contrib/apiOauth2Service.php');

$client = new apiClient();
$client->setApplicationName("Google Oauth2 Sample");

// Set your cached access token. Remember to replace $_SESSION with a real database or memcached.
if (isset($_SESSION['token'])) {
 $client->setAccessToken($_SESSION['token']);
}

$key = file_get_contents(G_KEY_FILE);

$cred = new apiAssertionCredentials(
  G_SERVICE_ACCOUNT_NAME,
  array(G_USER_PROFILE, G_USER_EMAIL),
  $key
);

print '<h2>JWT:</h2><pre>' . print_r($cred->generateAssertion(), true) . '</pre>';

$client->setAssertionCredentials($cred);

$client->setClientId(G_CLIENT_ID);
$service = new apiOauth2Service($client);

$result = $service->userinfo->get();
print '<h2>Oauth2 Result:</h2><pre>' . print_r($result, true) . '</pre>';

if ($client->getAccessToken()) {
  $_SESSION['token'] = $client->getAccessToken();
}

any thoughts?

--
Currenly, i have get

 "{"error":{"errors":[{"

message":"Forbidden"}],"code":403,"message":"Forbidden"}}"
when get data from google analytic api. I had create service account and I get access_token in Ruby successfully.
But i can't get data. It return above error.
How to get ids to attack on request: 

GET https://www.googleapis.com/analytics/v3/data/ga
  ?ids=ga:12345
  &start-date=2008-10-01
  &end-date=2008-10-31
  &metrics=ga:visits,ga:bounces

Ids is gotten from profile_id in google analytics
Any ideas.

--
If it helps anyone, i was getting a 400 error, until i changed "redirect_uri" in the POST request to be the same uri as the one i used as the "redirect_uri" parameter when i first retrieved the authorization token.

That fixed it for me. Might also help someone else :)

--
I'm facing an error 400 on the php api.
The strange thing, is that when i'm testing on my local computer it work perfectly an an other computer too, but on my server.
The only difference i've found for now is the return of openssl_pkey_get_private. wich give me something like: Resource id #34 on local but Resource id #33 on server.

Is there anyone who have faced this before?

--
I've print out the HttpRequest array, and they are exactly the same on both server and local computer. no package are missing or anything like that.
Everithing is going great, no errors until the response from the token server, who returned 400 invalid grant.


It's the just like the token server doesn't like my server ip adresse or something like that...

anyone have an idea of what i'm missing? 

--
We also have an identical use case, for periodically (not spamming your API!) downloading and storing GA data for further analysis.

It would be fantastic if you can support this in the GA API. :)

--
THANK YOU TANK YOU, A THOUSAND THANK YOU!!

I've been looking at the invalid request for two days until seeing your post.

--
I am having the same problem, when i try to connect against Google API in my local server, evrething goes fine, but when i try in my server on the web i can't do it. I have this problem a few days, did you solve the problem?

--
Is the redirect_url should absolutly be a https ?

--
No

--
PROBLEM SOLVED.

The time of my server was one minute in the future, compared with google's server time. So i put my server to update the time with ntpdate and now i don't have problem with invalid_grant anymore.

Hi, i had been same problem when i try to make request on webmaster tool and used token from OAuth2ServiceAccount and i got 403 code error.

is Webmaster tool supported in OAuth2ServiceAccount?

Hope anyone help me with this.

--
same error invalid_grant here .. it was the server time (was 5 min in the future compared to google time).

--

invalid_grant trying to get oAuth token from google


I keep getting an invalid_grant error on trying to get an oAuth token from Google to connect to their contacts api. All the information is correct and I have tripple checked this so kind of stumped.
Does anyone know what may be causing this issue? I have tried setting up a different client id for it but I get the same result, I have tried connecting many different ways including trying the force authentication, but still the same result.

--
I ran into this problem when I didn't explicitly request "offline" access when sending the user to the OAuth "Do you want to give this app permission to touch your stuff?" page.
Make sure you specify access_type=offline in your request.
(Also: I think Google added this restriction in late 2011. If you have old tokens from before then, you'll need to send your users to the permission page to authorize offline use.)

--
I ran into this same problem despite specifying the "offline" access_type in my request as per bonkydog's answer. Long story short I found that the solution described here worked for me:
In essence, when you add an OAuth2 Client in your Google API's console Google will give you a "Client ID" and an "Email address" (assuming you select "webapp" as your client type). And despite Google's misleading naming conventions, they expect you to send the "Email address" as the value of the client_id parameter when you access their OAuth2 API's.
This applies when calling both of these URL's:
⦁ https://accounts.google.com/o/oauth2/auth
⦁ https://accounts.google.com/o/oauth2/token
Note that the call to the first URL will succeed if you call it with your "Client ID" instead of your "Email address". However using the code returned from that request will not work when attempting to get a bearer token from the second URL. Instead you will get an 'Error 400' and an "invalid_grant" message.

--
Although this is an old question, it seems like many still encounter it - we spent days on end tracking this down ourselves.
In the OAuth2 spec, "invalid_grant" is sort of a catch-all for all errors related to invalid/expired/revoked tokens (auth grant or refresh token).
For us, the problem was two-fold:

1. User has actively revoked access to our app
Makes sense, but get this: 12 hours after revocation, Google stops sending the error message in their response: “error_description” : “Token has been revoked.”
It's rather misleading because you'll assume that the error message is there at all times which is not the case. You can check whether your app still has access at the apps permission page.

2. User has reset/recovered their Google password
In December 2015, Google changed their default behaviour so that password resets for non-Google Apps users would automatically revoke all the user's apps refresh tokens. On revocation, the error message follows the same rule as the case before, so you'll only get the "error_description" in the first 12 hours. There doesn't seem to be any way of knowing whether the user manually revoked access (intentful) or it happened because of a password reset (side-effect).
Apart from those, there's a myriad of other potential causes that could trigger the error:

3. Server clock/time is out of sync
4. Not authorized for offline access
5. Throttled by Google
6. Using expired refresh tokens
7. User has been inactive for 6 months
8. Use service worker email instead of client ID
9. Too many access tokens in short time
10. Client SDK might be outdated
11. Incorrect/incomplete refresh token
I've written a short article summarizing each item with some debugging guidance to help find the culprit. Hope it helps.

--
I encountered the same problem. For me, I fixed this by using Email Address (the string that ends with ...@developer.gserviceaccount.com) instead of Client ID for client_id parameter value. The naming set by Google is confusing here.

--
I had the same error message 'invalid_grant' and it was because the authResult['code'] send from client side javascript was not received correctly on the server.
Try to output it back from the server to see if it is correct and not an empty string.

--
My issue was that I used this URL:
https://accounts.google.com/o/oauth2/token
When I should have used this URL:
https://www.googleapis.com/oauth2/v4/token
This was testing a service account which wanted offline access to the Storage engine.

--
if you are using scribe library, just set up the offline mode, like bonkydog suggested here is the code:
OAuthService service = new ServiceBuilder().provider(Google2Api.class).apiKey(clientId).apiSecret(apiSecret)
                .callback(callbackUrl).scope(SCOPE).offline(true)
                .build();
this console board select your project input the oath url. the oauth callback url will redirect when the oauth success

--
Using a Android clientId (no client_secret) I was getting the following error response:
{
 "error": "invalid_grant",
 "error_description": "Missing code verifier."
}
I cannot find any documentation for the field 'code_verifier' but I discovered if you set it to equal values in both the authorization and token requests it will remove this error. I'm not sure what the intended value should be or if it should be secure. It has some minimum length (16? characters) but I found setting to null also works.
I am using AppAuth for the authorization request in my Android client which has a setCodeVerifier() function.
AuthorizationRequest authRequest = new AuthorizationRequest.Builder(
                                    serviceConfiguration,
                                    provider.getClientId(),
                                    ResponseTypeValues.CODE,
                                    provider.getRedirectUri()
                            )
                            .setScope(provider.getScope())
                            .setCodeVerifier(null)
                            .build();
Here is an example token request in node:
request.post(
  'https://www.googleapis.com/oauth2/v4/token',
  { form: {
    'code': '4/xxxxxxxxxxxxxxxxxxxx',
    'code_verifier': null,
    'client_id': 'xxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com',
    'client_secret': null,
    'redirect_uri': 'com.domain.app:/oauth2redirect',
    'grant_type': 'authorization_code'
  } },
  function (error, response, body) {
    if (!error && response.statusCode == 200) {
      console.log('Success!');
    } else {
      console.log(response.statusCode + ' ' + error);
    }

    console.log(body);
  }
);
I tested and this works with both https://www.googleapis.com/oauth2/v4/token and https://accounts.google.com/o/oauth2/token.
If you are using GoogleAuthorizationCodeTokenRequest instead:
final GoogleAuthorizationCodeTokenRequest req = new GoogleAuthorizationCodeTokenRequest(
                    TRANSPORT,
                    JSON_FACTORY,
                    getClientId(),
                    getClientSecret(),
                    code,
                    redirectUrl
);
req.set("code_verifier", null);          
GoogleTokenResponse response = req.execute();

--
This is a silly answer, but the problem for me was that I failed to realize I already had been issued an active oAuth token for my google user which I failed to store. The solution in this case is to go to the api console and reset the client secret.
There are numerous other answers on SO to this effect for example Reset Client Secret OAuth2 - Do clients need to re-grant access?

--
You might have to remove a stale/invalid OAuth response.
Note: An OAuth response will also become invalid if the password used in the initial authorization has been changed.
If in a bash environment, you can use the following to remove the stale response:
rm /Users/<username>/.credentials/<authorization.json>

--
After considering and trying all of the other ways here, here's how I solved the issue in nodejs with the googleapis module in conjunction with the request module, which I used to fetch the tokens instead of the provided getToken() method:
const request = require('request');

//SETUP GOOGLE AUTH
var google = require('googleapis');
const oAuthConfigs = rootRequire('config/oAuthConfig')
const googleOAuthConfigs = oAuthConfigs.google

//for google OAuth: https://github.com/google/google-api-nodejs-client
var OAuth2 = google.auth.OAuth2;
var googleOAuth2Client = new OAuth2(
    process.env.GOOGLE_OAUTH_CLIENT_ID || googleOAuthConfigs.clientId, 
    process.env.GOOGLE_OAUTH_CLIENT_SECRET || googleOAuthConfigs.clientSecret, 
    process.env.GOOGLE_OAUTH_CLIENT_REDIRECT_URL || googleOAuthConfigs.callbackUrl);

/* generate a url that asks permissions for Google+ and Google Calendar scopes
https://developers.google.com/identity/protocols/googlescopes#monitoringv3*/
var googleOAuth2ClientScopes = [
    'https://www.googleapis.com/auth/plus.me',
    'https://www.googleapis.com/auth/userinfo.email'
];

var googleOAuth2ClientRedirectURL = process.env.GOOGLE_OAUTH_CLIENT_REDIRECT_URL || googleOAuthConfigs.callbackUrl; 

var googleOAuth2ClientAuthUrl = googleOAuth2Client.generateAuthUrl({
  access_type: 'offline', // 'online' (default) or 'offline' (gets refresh_token)
  scope: googleOAuth2ClientScopes // If you only need one scope you can pass it as string
});

//AFTER SETUP, THE FOLLOWING IS FOR OBTAINING TOKENS FROM THE AUTHCODE


        const ci = process.env.GOOGLE_OAUTH_CLIENT_ID || googleOAuthConfigs.clientId
        const cs = process.env.GOOGLE_OAUTH_CLIENT_SECRET || googleOAuthConfigs.clientSecret
        const ru = process.env.GOOGLE_OAUTH_CLIENT_REDIRECT_URL || googleOAuthConfigs.callbackUrl
        var oauth2Client = new OAuth2(ci, cs, ru);

        var hostUrl = "https://www.googleapis.com";
        hostUrl += '/oauth2/v4/token?code=' + authCode + '&client_id=' + ci + '&client_secret=' + cs + '&redirect_uri=' + ru + '&grant_type=authorization_code',
        request.post({url: hostUrl}, function optionalCallback(err, httpResponse, data) {
            // Now tokens contains an access_token and an optional refresh_token. Save them.
            if(!err) {
                //SUCCESS! We got the tokens
                const tokens = JSON.parse(data)
                oauth2Client.setCredentials(tokens);

                //AUTHENTICATED PROCEED AS DESIRED.
                googlePlus.people.get({ userId: 'me', auth: oauth2Client }, function(err, response) {
                // handle err and response
                    if(!err) {
                        res.status(200).json(response);
                    } else {
                        console.error("/google/exchange 1", err.message);
                        handleError(res, err.message, "Failed to retrieve google person");
                    }
                });
            } else {
                console.log("/google/exchange 2", err.message);
                handleError(res, err.message, "Failed to get access tokens", err.code);
            }
        });
I simply use request to make the api request via HTTP as described here:https://developers.google.com/identity/protocols/OAuth2WebServer#offline
POST /oauth2/v4/token HTTP/1.1
Host: www.googleapis.com
Content-Type: application/x-www-form-urlencoded

code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7&
client_id=8819981768.apps.googleusercontent.com&
client_secret={client_secret}&
redirect_uri=https://oauth2.example.com/code&
grant_type=authorization_code
--
Try change your url for requst to
https://www.googleapis.com/oauth2/v4/token
--

Side scrolling by moving a 3x canvas


This project demonstrates a method to scroll a canvas left or right for a side-scrolling game.

Buttons and a textbox are provided to test different scroll speeds and to change the scrolling direction.

The layout used is based on a canvas that is three times the width of the device screen. Other arrangements are possible(a).

So that requires that the horizontal arrangment that contains the canvas be five times the width of the device screen.

Why? Because 1/3 of the canvas is always visible.

That means 2/3 of the canvas or two screen widths must have room to move out of view to the left and right of the device screen.

The canvas is pushed and pulled left and right by another horizontal arrangment to it's left (ScrollingCanvasX).

ScrollingCanvasX.Width is varied from 0 to two times the device screen width.

When it's width is set to 2 * screen width, the left side of the canvas is visible.

When it's width is set to 0, the right side of the canvas is visible.
For the airplane on the canvas to maintain it's position on the screen, it must move in the opposite direction of the canvas by the same amount.

So for each loop of the Scroll timer, ScrollingCanvasX.Width is subtracted from the airplane's x position.

With a background that is three times the device screen width, the left and right 1/3 of the canvas should be identical.

The middle 1/3 need not match the left and right 1/3s.

This demo uses responsive sizing and allows the device to be tilted horizontally or vertically.
---

(a) A canvas that is twice the device screen width is the minimum size required for scrolling. It would require a container that is three times the device width. The width of the spacer that pushes the canvas would range from zero to one screen width. The left and right sides of the background image would be identical. Canvases much larger than three times the device screen width may not function due to device memory limitations.





























--

search results for how to have a global variable and a text string in a label.


With another label for each of the three other variables. If you click Button1 before entering a value in each of number.Text, row.Text or quotient.Text without placing any numerical values in the text blocks, you will get a "this operation cannot accept the argument "empty string." message. You have no ...
... and string arguments have been passed to the increase procedure, all associations between these values and the global variable and label Text ...
I know there's been a few topics about a similar problem (I used the search function, promise), but it seems the typical fix is that a label is missing a ... so they could be used in other screens too (eg for 'purchasing' items), but it seems that trying to create a global variable for a label text is impossible.
Whenever the Calculate button is clicked, the 'x' variable (if any) in the given expression (first text box) is replaced with the number entered in the second ... In my example, I've used a label to display the result. ... Initialize a global variable " lblTxt" to *empty string*. ... I have attached the app's AIA file here.
GetValue calls look fine (provided you realize they aren't for the webDB) but you' re trying to use the compare texts block like as if it's the length text ... The CheckUsername procedure looks like it should work just fine; are you displaying that global variable on a label or a notifier somewhere if there's a ...
I think you're making this harder on yourself than is necessary. You are creating a global variable to store the local variable (argument) choice from the ...
What is an assessment list ( i have only a assessor label and text) ? ... What is assessment body ? and why did u assign it to an empty text string ? 3. ... Also, try my example first by filling in your own emal address in the global email variable and see if it works (do not forget to clear the TinyDB before you ...
That was so because, as we could read in the link you gave me, AI2 enabled the timer, but did not ackowledge its "fired" event until it would have arrived ... I tried following your advice, using a 2nd clock fire event to (re)print (every 500ms) the status label text with the contents of a global string variable set ...