What application type should I choose for a Power BI custom data connector

Hi everyone.

I’m developing a custom Power BI data connector, that will authorize with Auth0 using oAuth.

When end-users use this connector, they will have to login the first time. The custom data connector should then use refresh tokens, so it won’t need to ask for a new token again (power bi app’s will maybe refresh in the background, 2 times a day).

So in Auth0 I’m creating an application for this data-connector. What application type should I pick?

Hi Alex,

I am sorry, I am not familiar on how a Power BI connector is implemented, does it have a front-end and backend or it’s just a front-end ?. If you have backend, you can use a web application. Otherwise, it would be a Single Page Application that uses PKCE. The later is a public client so it does not need a secret, but you can not store refresh tokens on it (See this other article for that scenario, Refresh Token Rotation)
I hope that helps

Pablo.

1 Like

Thanks for contributing to this one @cibrax !

Hi @cibrax - thanks for helping out.

That are some really good questions. I don’t know if the Power BI connector can be treated as a backend, I’m trying to answer that know. Thanks for the help :relaxed:

1 Like

Hi @cibrax .

I found this article from microsoft regarding oAuth and custom data connectors;

Power Query extensions are evaluated in applications running on client machines. Data Connectors should not use confidential secrets in their OAuth flows, as users may inspect the extension or network traffic to learn the secret. See the Proof Key for Code Exchange by OAuth Public Clients RFC (also known as PKCE) for further details on providing flows that don’t rely on shared secrets. A sample implementation of this flow can be found on our GitHub site.

So does that mean application type of either native or SPA, if I understand correctly?

That actually helps a lot. Your connector is a public client, so you should configure it as SPA. That will use PCKE under the hood.

Best regards
Pablo

1 Like

Now that I have your attention :sweat_smile:

Would this flow allow for refresh tokens to be used? My ideal scenario is that the end-user will only authenticate once, when the custom data connector is “installed” for the first time. Do you think such a thing would be possible?

Yes, absolutelly! That is possible with refresh token rotation. See this,

Thanks
Pablo

2 Likes

Teamwork makes the dreamwork!

1 Like

I now have a working solution, where the custom data source connector uses the PCKE flow. It works great - and I don’t have to provide the client secret. Thanks for the awesome help :+1: Now I’m just trying to understand the potential gains/consequences if I enable rotating refresh tokens :thinking:

Hi Alex,
Please share your solution.
Regards,
Sjaak.

Hi @jjkaandorp.

Roger. So the solution from a Power BI custom data connector oAuth implementation with PCKE flow looks like this. If you have any questions or comments, please let me know. Some comments:

  • Make sure your auth0 login page supports IE 11
  • You need offline enabled for the below to work

Helper variables

client_id = "<AUTH0 CLIENT ID>"; 
redirect_uri = "https://oauth.powerbi.com/views/oauthredirect.html";
authorize_uri = "https://<YOUR_AUTH_DOMAIN>.com/authorize";
token_uri = "https://<YOUR_AUTH_DOMAIN>.com/oauth/token";
windowWidth = 1200;
windowHeight = 1000;
codeVerifier = Text.NewGuid() & Text.NewGuid();

Start login function

StartLogin = (resourceUrl, state, display) =>
    let
        params = Json.Document(resourceUrl),
        codeChallenge = Base64UrlEncodeWithoutPadding(Crypto.CreateHash(CryptoAlgorithm.SHA256, Text.ToBinary(codeVerifier, TextEncoding.Ascii))),
        AuthorizeUrl = authorize_uri & "?" & Uri.BuildQueryString([
            client_id = client_id,
            scope = "openid offline_access",
            response_type = "code",
            state = state,
            redirect_uri = redirect_uri,
            code_challenge_method = "S256",
            code_challenge = codeChallenge
            
            
            ])
    in
        [
            LoginUri = AuthorizeUrl,
            CallbackUri = redirect_uri,
            WindowHeight = windowHeight,
            WindowWidth = windowWidth,
            Context = []
        ];

Refresh your token

Refresh = (resourceUrl, refresh_token) =>
    let
         result =  TokenMethod("refresh_token", "refresh_token", refresh_token)
    in
        result;

Finish login

FinishLogin = (context, callbackUri, state) =>
    let
        parts = Uri.Parts(callbackUri)[Query],
        // if the query string contains an "error" field, raise an error
        // otherwise call TokenMethod to exchange our code for an access_token
        result = if (Record.HasFields(parts, {"error", "error_description"})) then 
                    error Error.Record(parts[error], parts[error_description], parts)
                 else
                    TokenMethod("authorization_code", "code", parts[code])
    in
        result;

Helper

Base64UrlEncodeWithoutPadding = (hash as binary) as text =>
    let
        base64Encoded = Binary.ToText(hash, BinaryEncoding.Base64),
        base64UrlEncoded = Text.Replace(Text.Replace(base64Encoded, "+", "-"), "/", "_"),
        withoutPadding = Text.TrimEnd(base64UrlEncoded, "=")
    in 
        withoutPadding;

TokenMethod

TokenMethod = (grantType, tokenField, code) =>
    let
        queryString = [
            grant_type = grantType,
            redirect_uri = redirect_uri,
            client_id = client_id,
            code_verifier = codeVerifier
        ],
        // hans =  Diagnostics.Trace(TraceLevel.Information, "TokenMethod: " & code, code),
        queryWithCode = Record.AddField(queryString, tokenField, code),

        tokenResponse = Web.Contents(token_uri, [
            Content = Text.ToBinary(Uri.BuildQueryString(queryWithCode)),
            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;
1 Like

Thanks for sharing that with the rest of community!

1 Like