Unsure if this is a "bug" per se, and from slack discussions, nothing on cdot code has changed recently. But multiple support engineers have noticed this as seemingly different from how it worked until now.
Recently (within the last week as of 2022-05-23), Support team has noticed customer's cDot accounts no longer being linked to their GitLab.com users. From admin point of view, "GitLab Groups" page shows Customer has not linked their GitLab.com account. even when the Customer record has a uid and gitlab provider. This creates an extra challenge for us on the support side due to reduced visibility on customer's namespaces.
From a customer POV, My Account > Your GitLab.com account shows No account linked.unless the view is loaded as a customer logging into cDot using their GitLab account.
The Provider is (and has been) gitlab and UID is 1688436 (my staging account)
The question here is: what changed and is it worth investigating?
It seems like nothing on cDot side has changed recently, so perhaps GitLab.com side?
Support impact
There are a few services on cDot side which do rely on user-level access token when available, for example Gitlab::HostedPlans::CreateTrialService. Support tooling relies on that service for "extending" expired subscriptions; it doesn't currently supply an admin token so it fails to work otherwise. Anecdotally speaking, this has worked fine up until very recently where we're now seeing accounts that had persisted access_tokens being invalidated.
Customer impact
For the customer purchasing perspective, it contributes to a bit of a confusing process when managing an existing subscription. Again, using my staging account (which was previously linked), trying to add minutes into an existing sub:
Sends me to the purchase URL (https://customers.staging.gitlab.com/subscriptions/new?plan_id=2c92a0086a07f4a8016a2c0a1f7b4b4c&subscription_id=A-S00082613&transaction=ci_minutes) now requiring an extra step to re-link: https://customers.staging.gitlab.com/auth/gitlab?redirect_to=https%3A%2F%2Fcustomers.staging.gitlab.com%2Fsubscriptions%2Fnew%3Fplan_id%3D2c92a0086a07f4a8016a2c0a1f7b4b4c%26subscription_id%3DA-S00082613%26transaction%3Dci_minutes
which resulted in a 404
Refreshed the page landed me back at the purchase workflow
Workarounds
Have the customer re-link their account, possibly every few hours?
Note for console users needing to workaround it: pass is_admin=true to BaseTrialService
The ability to opt-out of expiring access tokens was deprecated in GitLab 14.3 and removed in 15.0. All existing integrations must be updated to support access token refresh.
The general idea
Today we already have access_token in CDot Customer table. We need to save access_token_expires_at and refresh_token in Customer table. Every time when we use customer.access_token, we check whether the token should be refreshed. So, we can define a method Customer#access_token in Customer model.
Here is some quick outline(of what I think is required) to implement the access token refresh
1. Migrate Customer model to add new columns:
refresh_token
access_token_expires_at
2. Ensure gitlab.com API returns refresh_token and access_token_expires_at
refresh_token(to check or add)
expires_at(to check or add) Or, it can be calculated from created_at and expires_in(if these are available from API response)
access_token(already)
3. update CDot to save refresh_token and access_token_expires_at in everywhere.
There are multiple places where customer.access_token is saved. We need to save refresh_token and access_token_expires_at together.
Including trial user?
4. Implement Customer#refresh_token!. Changes in app/models/customer.rb might like (the codes are not tested)
SECONDS_BEFORE_REFRESH = 180 # 3 minutesdef access_token refresh_token! if should_refresh_token? self.attributes['access_token']enddef should_refresh_token? Time.current.to_i > access_token_expires_at - SECONDS_BEFORE_REFRESHenddef refresh_token! return unless refresh_token.present? oauth = OmniAuth::Strategies::GitLab.new( nil, Rails.application.secrets.gitlab_app_id, Rails.application.secrets.gitlab_app_secret, client_options: { site: "#{Rails.application.secrets.gitlab_url}/api/v4" } ) client = oauth.client token = OAuth2::AccessToken.new(client, nil, { refresh_token: refresh_token }) new_token = token.refresh! if new_token.present? self.update( access_token: new_token.token, access_token_expires_at: new_token.expires_at, refresh_token: new_token.refresh_token ) end # Question: do we want to log error(and report to Sentry) if token.refresh! fail?end
My understanding of this logic is that adding a new token will replace the previous one and make it invalid, but please confirm if that's correct.
@dnldnz This is also my understanding. For the same <application_id, resource_owner_id>, generating a new token(such as in the above codes we call OAuth2::AccessToken#refresh!) will invalidate the old token.
If we check the tokens from the database(table oauth_access_tokens), we could see data like the below. Look at the revoked_at and created_at, I think: when OAuth generates a new token, it always revokes the old token first.
Looking at the source code you shared, Is it possible that a specific user can only have a single active OAuth "refresh token" at a given time?
So yes, for the specific <user, application> combination, it can only have a single active refresh_token(and only a single active access_token at most) at a given time.
Such some customers should be a pretty small percentage. Roughly estimation is: about 0.035%(985 / (2825278 + 985)). These customers's access tokens still have expires_in: nil.
Users with expired access token unable to apply for a trial or any request to the GitLab API as their token expired.
Solution
We are dealing with 2 problems here:
lack of ability to refresh the access token from the CustomersDot side
customers with expired tokens who can't access the GitLab API on behalf of CustomersDot application.
Solution for 1st problem: When the user authenticates on CustomersDot via GitLab OAuth we should store refresh_token and use it to fetch the new access token if it is expired.
Solution for 2nd problem: Because access tokens that we store on the CustomersDot are no longer valid we have to nullify them. I propose to write a simple Rake task that will go through all users on CustomersDot side, make a call to a special GitLab API in order to receive an information about access token: if token invalid the nullify it.
I would choose to take these actions:
First, implement solution for problem 2: nullify invalid access_token to quickly alleviate customer impact in short term.
And then, implement solution for problem 1: add token refresh support for the long term. The solution could follow #4345 (comment 957695713).
Solution for 2nd problem: Because access tokens that we store on the CustomersDot are no longer valid we have to nullify them. I propose to write a simple Rake task that will go through all users on CustomersDot side, make a call to a special GitLab API in order to receive an information about access token: if token invalid the nullify it.
7443 access_tokens failed to nullify, due to the customers being in rejected countries.
These are the 12 rejected countries: ["Cuba", "Hong Kong", "United States Minor Outlying Islands", "Belarus", "Virgin Islands (U.S.)", "Russian Federation", "Macao", "Iran (Islamic Republic of)", "Korea, Democratic People's Republic of", "China", "Syrian Arab Republic", "Sudan"].
I have a question: for these rejected countries' customers, are they still allowed to login Gitlab.com and CustomersDot portal to view/manage their existing subscriptions? If yes, we should nullify their access_token the same way as customers from other countries? @tgolubeva maybe you know the answer?
Per this slack discussion: for these rejected countries, existing customers are also blocked by default and they are redirected to Sales for manual review..
I think it is not urgent/important to nullify access_token for the 12 rejected countries.
I will focus on enabling access_token refresh. After token refresh is supported, we do not need to worry about the nullify access_token anyway. Please correct me if you have concerns. Thanks.
Adding the Support Priority and Support Efficiency labels since this is kind of a blocker for some of our workflows. Most specifically, the team is blocked from "extending" subscriptions (which creates a trial) since the trial creation relies on the Customer's UID, which isn't working because of this bug.
note for console users needing to workaround it: pass is_admin=true to BaseTrialService
ETA:
here is an example of all the steps we have to jump thru to support customers during this
There seems to be some weird thing going on with CustomersDot. From our admin view, it seems the account isn't linked but when the customer use the Sign in with your GitLab.com account, it takes the customer to the linked account anyway.
Please note that at the time we ask the customer for screen recording, we don't see any account link. The customer provide the screenshot and we still don't see any account linked.
when link Gitlab.com account or Sign in with your GitLab.com account, it will set a new acccess_token. The new access_token will be valid within 2 hours.
after 2 hours the access_token expired. Now if we login CustomersDot using username + password(this could be customer login using username/password, or admin impersonates customer login), this will set access_token: nil and thus the previously linked account is unlinked again.
From our admin view, it seems the account isn't linked
As said above, it this happens after access_token expired(expire in 2 hours), it will unlink the account.
but when the customer use the Sign in with your GitLab.com account, it takes the customer to the linked account anyway.
As said above, this will set a new access_token and link the account. This access_token will be valid for 2 hours.
In this screen recording, it does not check account details. Or you might mean to check from admin view again? As said above, if login CDot using username/password(or an admin impersonates this customer login) after access_token expired, it will unlink the account.
OTOH, the ticket(https://gitlab.zendesk.com/agent/tickets/300112)'s remaining issue is: when user Sign in with GitLab.com account, it logins another previously linked account. Because there were two customer accounts had ever been linked to the same Gitlab.com account, this confused the system. This seems like a scenario CustomersDot may not have handled correctly? I would prefer to create another issue for this, as this seems not related to access_token.
OTOH, the ticket(https://gitlab.zendesk.com/agent/tickets/300112)'s remaining issue is: when user Sign in with GitLab.com account, it logins another previously linked account. Because there were two customer accounts had ever been linked to the same Gitlab.com account, this confused the system. This seems like a scenario CustomersDot may not have handled correctly? I would prefer to create another issue for this, as this seems not related to access_token.
I see you mentioned in the other issue. You're right, it's possible that the problem exist for admin view because the account still have uuid linked. I'll go ahead and unlink it for them via console.
it's possible that the problem exist for admin view because the account still have uuid linked. I'll go ahead and unlink it for them via console.
I believe if we set the uid: nil, provider: nil, access_token: nil, it should work.
The reason why the user is directed to the unexpected Customer account is this code:
def init_customer Customer.with_oauth_info(params[:uid], params[:provider]).last || Customer.new end
When signin with GitLab.com account, it will find the existing CustomersDot customer by uid and provider, and if there are multiple one, it returns the last(order by customer.id) one.