DataConnectors icon indicating copy to clipboard operation
DataConnectors copied to clipboard

Failing to refresh token because of multiple requests to refresh authentication token using PKCE method

Open joefields opened this issue 2 years ago • 16 comments

We have a custom connector that is using the PKCE OAuth2 flow based around the provided example here and we are having users report occasional issues where they need to sign in again on Power BI Desktop to refresh their data. In looking at API logs on our end, it appears we have some cases where we see repeat calls to use the same refresh token to fetch an updated authentication token in a short period. We speculate there is parallel loading of various tables in Power Query and multiple queries are trying to use the same refresh token to refresh the authentication token. The first attempt to use the refresh token is successful with a 200 response but all subsequent calls using the same refresh token receive a 400 status with an error message which breaks the refresh and the users have to login again.

Here is a snippet of the authentication from our connector:

StartLogin = (resourceUrl, state, display) =>
    let
        clientId = getClientIdByRegion(resourceUrl),
        // We'll generate our code verifier using Guids
        codeVerifier = Text.NewGuid() & Text.NewGuid(),
        AuthorizeUrl = authorize_uri & "?" & Uri.BuildQueryString([
            client_id = clientId,
            response_type = "code",
            code_challenge_method = "plain",
            scope="",
            code_challenge = codeVerifier,
            state = state,
            redirect_uri = redirect_uri])
    in
        [
            LoginUri = AuthorizeUrl,
            CallbackUri = redirect_uri,
            WindowHeight = 720,
            WindowWidth = 1024,
            // Need to roundtrip this value to FinishLogin
            Context = codeVerifier
        ];

// The code verifier will be passed in through the context parameter.
FinishLogin = (c, dataSourcePath, context, callbackUri, state) =>
    let
        Parts = Uri.Parts(callbackUri)[Query]
    in
        TokenMethod(dataSourcePath, Parts[code], "authorization_code", context);

TokenMethod = (dataSourcePath, code, grant_type, optional verifier) =>
    let
        // region = Record.Field(dataSourcePath, "region"),
        clientId = getClientIdByRegion(dataSourcePath),
        codeVerifier = if (verifier <> null) then [code_verifier = verifier] else [],
        codeParameter = if (grant_type = "authorization_code") then [ code = code ] else [ refresh_token = code ],
        query = codeVerifier & codeParameter & [
            client_id = clientId,
            grant_type = grant_type,
            redirect_uri = redirect_uri
        ],

        ManualHandlingStatusCodes= {},
        
        Response = Web.Contents(base_path & "/authentication" & "/token", [
            Content = Text.ToBinary(Uri.BuildQueryString(query)),
            Headers = [
                #"Content-type" = "application/x-www-form-urlencoded",
                #"Accept" = "application/json"
            ],
            ManualStatusHandling = ManualHandlingStatusCodes
        ]),
        Parts = Json.Document(Response)
    in
        // check for error in response
        if (Parts[error]? <> null) then 
            error Error.Record(Parts[error], Parts[message]?)
        else
            Parts;
 
Refresh = (ca, resourceUrl, oldCredentials) => TokenMethod(resourceUrl, oldCredentials[refresh_token], "refresh_token");

How can we ensure there is only one call to refresh the authentication token when there are multiple queries that depend on the authentication token?

joefields avatar Jun 16 '22 23:06 joefields