cancel
Showing results for 
Search instead for 
Did you mean: 
Reply
halfglassdarkly
Resolver I
Resolver I

Oauth2 flow with custom connector requiring username and password

Hi all, long time lurker first time poster.

 

I usually try to solve things myself but this currently has me stumped. Apologies in advance for the long-winded post.

 

I'm trying to get PowerQuery/PowerBI to connect to the New Zealand Business Number (NZBN) API using a 2 legged OAuth 2 flow with client_credentials. In my test query I am using their test API call called MBIE Echo which takes an input string and returns it.

 

I have got this working successfully in Postman, and with the aid of Jussi Roine's useful tutorial I have managed to get this working in PowerQuery by writing a custom connector using the Power Query SDK for Visual Studio.

 

In PowerQuery I am able to connect successfully to the API including initiating a token refresh, and it is returning data as expected.

However, in order to connect, PowerQuery is prompting me to authenticate with my user name and password for the RealMe identification service that the NZ Government uses for a number of websites, and which is used by the Ministry of Business, Innovation & Employment (MBIE) who own the API, when accessing the API console via their website.

 

Using Fiddler for traffic inspection, I can see this is hitting the login.realme.govt.nz server which returns some cookies and then refers me to api.business.govt.nz and then on to oauth.powerbi.com for the OAuth redirect...

 

 

Fiddler.jpg

 

The query then runs successfully and returns the expected response. It will also refresh fine. The problem is, if I make any change to the query, it requires re-authorization with username and password. This includes when using parameters to change the content of the get request to MBIE Echo - if I create a custom function out of the query to pass parameters to the query from another table I am prompted to reenter username and password for every record for which the function is invoked, which obviously makes this unusable in a real-world situation.

 

 

I'm very new to working with OAuth 2 and only have a vague idea what I'm doing, but it seems to me like maybe this is an issue with PowerQuery not storing/reading the cookie returned during the RealMe authentication step, or maybe there is a way to bypass this authentication step entirely that I am missing when writing my connector. In Postman I am able to initiate the OAuth token refresh and MBIE Echo API calls using the client_id and client_secret without being prompted for the RealMe username and password as well, so I would have thought the same should be able to be implemented using the PowerQuery connector?

 

In case it made a difference I have set my PQ/PBI Security settings to not check for certificate revocation information, and I also  enabled the 'allow any extension to load without validation or warning' before using the custom connector.

 

halfglassdarkly_7-1632008388569.png

Note: for some types of API requests MBIE does require a 3 legged OAuth flow, but for the type of calls I am working with 2 legged flow should be all that is required. See :

https://api.business.govt.nz/api/apis/info?name=NZBN&version=v4&provider=mbiecreator and

https://support.api.business.govt.nz/s/article/oauth2-authentication

 

Below is the text from the .pq file from my custom connector. The client_id and client_secret are stored in seperate text files.

 

section NZBN_Register;
// NZBN_Register OAuth2 values
client_id = Text.FromBinary(Extension.Contents("client_id.txt"));
client_secret = Text.FromBinary(Extension.Contents("client_secret.txt"));
redirect_uri = "https://oauth.powerbi.com/views/oauthredirect.html";
token_uri = "https://api.business.govt.nz/services/token?grant_type=client_credentials";
authorize_uri = "https://api.business.govt.nz/services/authorize";
logout_uri = "https://login.microsoftonline.com/logout.srf";
// Login modal window dimensions
windowWidth = 720;
windowHeight = 1024;
[DataSource.Kind = "NZBN_Register", Publish = "NZBN_Register.Publish"]
shared NZBN_Register.Contents = (url as text) =>
let
source = Json.Document(Web.Contents(url))
in
source;
// Data Source Kind description
NZBN_Register = [
TestConnection = (dataSourcePath) => {"NZBN_Register.Contents", dataSourcePath},
Authentication = [
OAuth = [
StartLogin = StartLogin,
FinishLogin = FinishLogin,
Refresh = Refresh,
Logout = Logout
]
],
Label = Extension.LoadString("DataSourceLabel")
];
// Data Source UI publishing description
NZBN_Register.Publish = [
Beta = true,
Category = "Other",
ButtonText = {Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp")},
LearnMoreUrl = "https://powerbi.microsoft.com/",
SourceImage = NZBN_Register.Icons,
SourceTypeImage = NZBN_Register.Icons
];
// Helper functions for OAuth2: StartLogin, FinishLogin, Refresh, Logout
StartLogin = (resourceUrl, state, display) =>
let
authorizeUrl = authorize_uri
& "?"
& Uri.BuildQueryString(
[
response_type = "code",
client_id = client_id,
redirect_uri = redirect_uri,
content_type = "application/x-www-form-urlencoded",
state = state,
scope = "am_application_scope default",
Accept = "application/json"
]
)
in
[
LoginUri = authorizeUrl,
CallbackUri = redirect_uri,
WindowHeight = 720,
WindowWidth = 1024,
Context = null
];
FinishLogin = (context, callbackUri, state) =>
let
// parse the full callbackUri, and extract the Query string
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;
Refresh = (resourceUrl, refresh_token) =>
TokenMethod("refresh_token", "refresh_token", refresh_token);
Logout = (token) => logout_uri;
// see "Exchange code for access token: POST /oauth/token" at https://cloud.ouraring.com/docs/authentication for details
TokenMethod = (grantType, tokenField, code) =>
let
queryString = [
grant_type = "client_credentials",
redirect_uri = redirect_uri,
client_id = client_id,
client_secret = client_secret
],
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;
Value.IfNull = (a, b) => if a <> null then a else b;
GetScopeString = (scopes as list, optional scopePrefix as text) as text =>
let
prefix = Value.IfNull(scopePrefix, ""),
addPrefix = List.Transform(scopes, each prefix & _),
asText = Text.Combine(addPrefix, " ")
in
asText;
NZBN_Register.Icons = [
Icon16 = {
Extension.Contents("NZBN_Register16.png"),
Extension.Contents("NZBN_Register20.png"),
Extension.Contents("NZBN_Register24.png"),
Extension.Contents("NZBN_Register32.png")
},
Icon32 = {
Extension.Contents("NZBN_Register32.png"),
Extension.Contents("NZBN_Register40.png"),
Extension.Contents("NZBN_Register48.png"),
Extension.Contents("NZBN_Register64.png")
}
];

 

And this is the test query I am running in PowerQuery using the MBIE Echo API:

 

let
Source = NZBN_Register.Contents("https://sandbox.api.business.govt.nz/services/v1/echo/echoString?in=hello world"),
echoStringResponse = Source[echoStringResponse],
#"Converted to Table" = Record.ToTable(echoStringResponse)
in
#"Converted to Table"

 

Which returns my hello world text back as a string before being converted to a table.

 

halfglassdarkly_1-1632007700115.png

 

If I attempt to change the query, e.g. setting the echo string to "new string"

 

Source = NZBN_Register.Contents("https://sandbox.api.business.govt.nz/services/v1/echo/echoString?in=new string")

 

I will get the edit credentials error prompt:

 

halfglassdarkly_2-1632007905407.png

 

halfglassdarkly_3-1632007955515.png

On signing in the query runs as expected.

halfglassdarkly_4-1632008070932.png

halfglassdarkly_5-1632008172860.png

 

halfglassdarkly_6-1632008202006.png

 

It's really frustrating getting this far with successful API calls but not knowing how to proceed in dealing with these credentials prompts. Any help would be most appreciated! - Daniel

3 REPLIES 3
halfglassdarkly
Resolver I
Resolver I

So I haven't had concentrated time this week to solve this yet, but @ImkeF's suggestion has definitely pointed me in the right direction.

 

I tried implementing a relative path in the query per the suggested blog post, and it returned an error (which I failed to save the text of but essentially: invalid number of arguments, expected 1 received x).

 

Since I'm using a custom connector, I've come to the conclusion I need to make changes to the connector rather than my query so queries using the connector expect multiple inputs (query string of URL based on parameter).

 

The section of this article on excluding required parameters from your data source path covering was helpful, and I was able to successfully replecate their example of excluding parameters from authentication in the HelloWorldWithDocs tutorial connector using 'DataSource.Path = false'. I knew I shouldn't have skipped the Hello World tutorial ><.

 

Now I need to work out how to implement that in my connector as that tutorial doesn't cover how to implement this for the query string in a URL in a connector. I'm sure I will get there eventually, but any suggestions appreciated.

 

As best I can tell, this is the section of my custom connector .pq file that I need to modify?

 

let
queryString = [
grant_type = "client_credentials",
redirect_uri = redirect_uri,
client_id = client_id,
client_secret = client_secret
],
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;

 

ImkeF
Super User
Super User

Hi @halfglassdarkly ,

try to split up the url into a main part (which should be the same for all calls and hopefully controls the auth) and eiter a relative path or query parameter.

You can see both methods desribed (and a lot of useful links) in this blogpost : http://blog.datainspirations.com/2018/02/17/dynamic  (Web view)

Imke Feldmann (The BIccountant)

If you liked my solution, please give it a thumbs up. And if I did answer your question, please mark this post as a solution. Thanks!

How to integrate M-code into your solution -- How to get your questions answered quickly -- How to provide sample data -- Check out more PBI- learning resources here -- Performance Tipps for M-queries

That's great, thanks @ImkeF I will give that a go and report back.

Helpful resources

Announcements
UG GA Amplification 768x460.png

Launching new user group features

Learn how to create your own user groups today!

November Power BI Update 768x460.png

Check it Out!

Click here to read more about the November 2021 Updates!

M365 768x460.jpg

Microsoft 365 Collaboration Conference | December 7–9, 2021

Join us, in-person, December 7–9 in Las Vegas, for the largest gathering of the Microsoft community in the world.

Top Kudoed Authors