Forum Discussion
Custom Connector failing on refreshing data with multiple calls to get a refresh token
We have a similar 200 response which contains the access_token, expires_in <seconds>, and refresh_token.
The issue is the new refresh_token is one-time use where as it appears in your case the refresh_token might be re-usable within a narrow window of time.
Here is the 400 response:
I am looking to see if there is anything in the connector code that we can do to limit the refresh to only one thread/instance or if we can ignore a 400 response as long as we have one 200 response with an updated access_token.
from Microsoft oauth flow - return of a new token refresh:
| refresh_token | A new OAuth 2.0 refresh token. Replace the old refresh token with this newly acquired refresh token to ensure your refresh tokens remain valid for as long as possible. Note: Only provided if offline_access scope was requested. |
When you refresh your token a new token is generated that is the default.
i tried on my side and the BPI is sending and saving the correct token.
try to implement the advanced OAuth Handling authentication for Power Query connectors - Power Query | Microsoft Docs
here is my implementation
redirect_uri = "https://oauth.powerbi.com/views/oauthredirect.html"; // needs to be added as valid redirect URL (Desktop App --> PKCE!)
logout_uri = "https://login.microsoftonline.com/logout.srf";
baseAuthentificationURL = (tenant) => "https://login.microsoftonline.com/" & tenant & "/oauth2/v2.0";
windowWidth = 1200;
windowHeight = 1000;
//OpenID method to get token endpoints on demand.
openIDconfig = (dataSourcePath) =>
let
//host = Uri.Parts(dataSourcePath)[Host],
queryURL = "https://login.microsoftonline.com/" & Settings(dataSourcePath)[tenantName] & "/v2.0/.well-known/openid-configuration",
queryString = [],
response = Web.Contents(queryURL, [
Headers = [
#"Accept" = "application/json"
],
ManualStatusHandling = {400}
]),
body = Json.Document(response),
result = if (Record.HasFields(body, {"error", "error_description"})) then
error Error.Record(body[error], body[error_description], body)
else
body
in
result;
//will be called as first function on login.
StartLogin = (clientApplication, dataSourcePath, state, display) =>
try
let
codeVerifier = Text.NewGuid() & Text.NewGuid(), //needed for PKCE this will remove the need for a client secret.
authorization_endpoint = openIDconfig(dataSourcePath)[authorization_endpoint],
AuthorizeUrl = authorization_endpoint & "?" & Uri.BuildQueryString([
client_id = Settings(dataSourcePath)[client_id],
redirect_uri = redirect_uri,
response_type = "code",
code_challenge_method = "plain", //needed for PKCE
code_challenge = codeVerifier, //needed for PKCE
scope=Settings(dataSourcePath)[client_id] & "/user_impersonation offline_access" //only if offline_access is requested a refresh_token is provided.
])
in
[
LoginUri = AuthorizeUrl,
CallbackUri = redirect_uri,
WindowHeight = windowHeight,
WindowWidth = windowWidth,
Context = codeVerifier
]
otherwise
let
message = Text.Format("Start Login Methode Error")
in
Diagnostics.Trace(TraceLevel.Error, message, () => error message, true)
;
//after the user has done the MFA this function will be called
FinishLogin = (clientApplication, dataSourcePath, context, callbackUri, state) =>
let
parts = Uri.Parts(callbackUri)[Query],
result = if (Record.HasFields(parts, {"error", "error_description"})) then
error Error.Record(parts[error], parts[error_description], parts)
else
TokenMethod(dataSourcePath, "authorization_code", parts[code], context)
in
result
;
//in the case of first login or token refresh this function will be called and request an (new) access token
TokenMethod = (dataSourcePath, grant_type, tokenFieldValue, optional verifier) =>
let
codeVerifier = if (verifier <> null) then [code_verifier = verifier] else [],
codeParameter = if (grant_type = "authorization_code") then [ code = tokenFieldValue ] else [ refresh_token = tokenFieldValue ],
queryString = codeVerifier & codeParameter & [
client_id = Settings(dataSourcePath)[client_id],
grant_type = grant_type,
redirect_uri = redirect_uri
],
token_endpoint = openIDconfig(dataSourcePath)[token_endpoint],
tokenResponse = Web.Contents(token_endpoint, [
Content = Text.ToBinary(Uri.BuildQueryString(queryString)),
Headers = [
#"Content-type" = "application/x-www-form-urlencoded",
#"Accept" = "application/json"
],
ManualStatusHandling = {400}
]),
body = Json.Document(tokenResponse),
result = if (Record.HasFields(body, {"error", "error_description"})) then
error Error.Record(body[error], body[error_description], body)
else
body
in
result;
//Called on token experation
Refresh = (clientApplication, dataSourcePath, oldCredential) =>
let
refreshToken = oldCredential[refresh_token],
result = TokenMethod(dataSourcePath, "refresh_token", refreshToken)
in
result;
//Called on Logout
Logout = (clientApplication, dataSourcePath, accessToken) => logout_uri;