PDA

View Full Version : oAuth2 and Web Application



SEHS
16-Mar-2020, 11:17 AM
Hi!

I'm working with a integration that requires oAuth2 authentication. Has anyone any idea of how this can be done? I have seen multiple examples but they seems to show only how this is done in a Windows application.

My idea is to have a button in the WebApp that says: "Connect to Visma eAccounting" and the user will then be able to log in to their Visma-account, before they will be redirected back to my web application. Somewhere along this function, I need to obtain the authorization code that Visma responds with.


Documentation:
https://developer.vismaonline.com/docs/getting-started

Mike Peat
16-Mar-2020, 12:52 PM
Hi

That's actually (part of) what this forum is about. Check out the DataFlex OAuth2 component: https://support.dataaccess.com/Forums/attachment.php?attachmentid=9277&d=1443020867. The documentation is here: https://docs.google.com/document/d/14Kvk0C-vZXBrORh6IjlEzk63Z1KuYHQ3vL5CzpXHXmY/edit?usp=sharing. The component comes with a workspace with a sample web application which allows you to connect to a number of services (some, especially Google, may have gone stale over time), which should get you started on how to connect your own app to Visma. Yes, the views in that web app have buttons which say "Connect to XYZ". :)

Mike

SEHS
20-Mar-2020, 12:43 PM
Thanks!

I have installed it and copied the OAuth2 folder to my workspaces AppHtml folder. I have also included the JS-file in my index.html as described in the documentation. Still, when i run the web application I get an error: "Unhandled Program Error on the client. Could not find the class 'df.OAuth2'". Pretty sure there is something obvious I have forgot, but I can't figure out what.

Mike Peat
20-Mar-2020, 01:39 PM
Hmm... you are "Use"ing the cDFOAuth2.pkg in your view and created an object of the cOAuth2 class there - I think you must have, or you wouldn't get that error....

What I do in this sort of circumstance is to use the web browser's debugger to check that the df.OAuth2 code it actually getting there.

Mike

DaveR
20-Mar-2020, 01:47 PM
Mike

brevity is the soul of wit, they say

Mike Peat
20-Mar-2020, 01:52 PM
Dave - I realised after posting that I was addressing the wrong question and edited/deleted it - now replaced with a suggestion. ;)

Mike

DaveR
20-Mar-2020, 05:17 PM
Dave - I realised after posting that I was addressing the wrong question and edited/deleted it - now replaced with a suggestion. ;)

Mike
:cool: sorry, I'm working from home and even more easily amused than usual...

SEHS
1-Apr-2020, 04:04 PM
Hi!

Thank you for the reply. I figured it out :)

I am now facing another issue due to this integration that I hope you might have an idea on have to solve.

This is a POST request with x-www-form-urlencoded body.



Object oVismaAuthTransfer is a cHttpTransfer
Property UChar[] pucaVismaAuthData

Set psRemoteHost to "identity-sandbox.test.vismaonline.com"
Set piRemotePort to rpHttpSSL
Set peTransferFlags to ifSecure

Procedure OnDataReceived String sContentType String sData
UChar[] ucaVismaAuthData

Get pucaVismaAuthData to ucaVismaAuthData
Move (AppendArray(ucaVismaAuthData, StringToUCharArray(sData))) to ucaVismaAuthData
Set pucaVismaAuthData to ucaVismaAuthData
End_Procedure

Procedure Reset
UChar[] empty

Set pucaVismaAuthData to empty
End_Procedure
End_Object

Function fVismaGetAccessToken Integer iKrednr Returns String
Boolean bOK
Integer i iSizeOfArray iResponseCode
Handle hoJSON
String sRefreshToken sAccessToken sTokenType sClientId sClientSecret sURL
tKreditorIntegrasjonKey[] stKIK
tIntegrasjonKey[] stIK

Get fKreditorIntegrasjonKeysPerIntegrasjon of oSqlFunc iKrednr "eAccounting" (&stKIK) to bOK
If (bOK) Begin
Move (SizeOfArray(stKIK)) to iSizeOfArray
For i from 0 to (iSizeOfArray-1)
If (Lowercase(stKIK[i].Navn)="refresh_token");
Move (Trim(stKIK[i].Verdi)) to sRefreshToken
Loop
If (sRefreshToken>"") Begin
Get fIntegrasjonKeysPerIntegrasjon of oSqlFunc "eAccounting" (&stIK) to bOK
If (bOK) Begin
Move (SizeOfArray(stIK)) to iSizeOfArray
For i from 0 to (iSizeOfArray-1)
If (Lowercase(stIK[i].Navn)="clientid");
Move (Trim(stIK[i].Verdi)) to sClientId
If (Lowercase(stIK[i].Navn)="clientsecret");
Move (Trim(stIK[i].Verdi)) to sClientSecret
Loop
// STARTS HERE
// Moving this to sURL to be used as the x-www-form-urlencoded body:
Move (SFormat("grant_type=%1&refresh_token=%2&redirect_uri=%3", "refresh_token", sRefreshToken, "https://localhost:44300/callback")) to sURL
Get Base64EncodeString (sClientId+":"+sClientSecret) to sClientSecret
Send Reset of oVismaAuthTransfer
Send ClearHeaders of oVismaAuthTransfer
Get AddHeader of oVismaAuthTransfer "Authorization" ("Basic"*sClientSecret) to bOK
Get AddHeader of oVismaAuthTransfer "Content-Type" "application/x-www-form-urlencoded" to bOK
Get HttpPostRequest of oVismaAuthTransfer "connect/token" sURL False to bOK

If (bOK) Begin
Get ResponseStatusCode of oVismaAuthTransfer to iResponseCode
If ((iResponseCode>=200) and (iResponseCode<300)) Begin
Get Create (RefClass(cJsonObject)) to hoJSON
Get ParseUtf8 of hoJSON (pucaVismaAuthData(oVismaAuthTransfer(Self))) to bOK
If (bOK) Begin
If (HasMember(hoJSON, "access_token")) Begin
Get MemberValue of hoJSON "access_token" to sAccessToken
Get MemberValue of hoJSON "token_type" to sTokenType
Get MemberValue of hoJSON "refresh_token" to sRefreshToken
Move (sTokenType*sAccessToken) to sAccessToken
End
End
Send Destroy of hoJSON
End
Else Begin
Send UserError "Message" "Header"
End
End
End
End
End
Function_Return sAccessToken
End_Function


This returns 400 - Bad Request to iStatusCode

Mike Peat
5-Apr-2020, 07:14 AM
OK, I've been staring at this for a while. I am not clear whether you are requesting an initial access token or a refresh token.

I personally don't much like SFormat - I prefer to build up my strings bit by bit where I can see them - however it looks to me as though that might be where the problem is.

In the docs (https://developer.vismaonline.com/docs/getting-started) it says the POST body for requesting an access token should read:"grant_type=authorization_code&code=<authorization_code>&redirect_uri=<redirect_uri>".

AFAICS, your line of code: Move (SFormat("grant_type=%1&refresh_token=%2&redirect_uri=%3", "refresh_token", sRefreshToken, "https://localhost:44300/callback")) to sURL will result in sURL (which would be less confusingly called sBody) being: "grant_type=refresh_token&refresh_token=<sRefreshToken>&redirect_uri=https://localhost:44300/callback".

So this is where I am confused - are you requesting an access token or a refresh token?

Personally I'd lose the SFormat stuff - from my PoV it just makes things harder to read.

I'd do:


String[] asPaths
String sBody

// Assummes sToken already contains the authorization code and that
// sRedir contains your redirect URL: "https://localhost:44300/callback"

Move ("grant_type=authorization_code") to asParts[0]
Move ("code=" + sToken) to asParts[1]
Move ("redirect_uri=" * sRedir) to asParts[2]
Move (StrJoinFromArray(asParts, "&")) to sBody


OTOH if you are actually requesting a refresh token, you need to lose the last bit about the redirect uri, as the doc says that should just be: "grant_type=refresh_token&refresh_token=<refresh_token>".

Mike

SEHS
28-May-2020, 08:55 AM
Hi again!

Almost done with the integration, just the connection from the web application that remains.

I am working with your library, and I'm not sure if I know OAuth2 enough to understand quite what's wrong, so I hope that you (or someone else) can help me.

Step-by-step of what I am doing:

1. Click the "Connect to Visma" button.
2. This site pops up (Attachment: 1.jpg)
13723

3. After signing in, it will redirect you to this site where you gives the application permission. (Attachment: 2.jpg)
13724

4. Clicking "Yes, allow" will redirect you to the site written in the wpsRedirectUrl-property.
5. Get the value from "code" in the URL (failes).

Here is what my properties looks like:


// Web properties
Set wpsOAuth2Url to "https://identity-sandbox.test.vismaonline.com/connect/authorize"
Set wpsClientID to "ClientId"
Set wpsRedirectUrl to "https://domain.com/OAuth2/Callback.html"
Set wpsResponseType to "code"
Set wpsAuthCdName to "code"

// Normal properties
Set psClientSecret to "ClientSecret"

Mike Peat
28-May-2020, 11:25 AM
OK, here is what I'd do...

With your app open in the browser (in debug-run from the DF Studio), but before you attempt to log in to the service, right-click somewhere on the browser page and from the pop-up menu select "Inspect" (in Chrome) or "Inspect Element (Q)" (in Firefox), then in the window that opens up go to the "Sources" tab (in Chrome) or the "Debugger" tab (in Firefox). In the left-hand pane of that, navigate to the oOAuth2.js file.

Set a breakpoint (click the line number) just after the "url = " line:


var pollTimer = window.setInterval(function() {

try {

if (win.document.URL.indexOf(obj.wpsRedirectUrl) != -1) {
window.clearInterval(pollTimer);
url = win.document.URL;
win.close(); // <-- set the breakpoint here



(About line 81 or 82 in my version.)

Then go through your login procedure (click the button or whatever) and perform the login - you should then hit the breakpoint in the browser's debugger.

Look at what is in that url variable (hovering over it will generally show you, but find it in the Scopes pane of the browser's debugger which might make it easier you to copy it's content). Then paste that into a text editor for examination:

It will be something like this example from Google:


"http://localhost/GoogleAPI181/MSCallback.html?code=AQABAAIAAAAm-06blBE1TpVMil8KPQ411ocdZfH2nYgskb3A3XX52FN_LvA9tti 9oLBSAEvuOD2KcjnWt4G1hoHXIqem8lVikMUnPlTKZKmqCHF_H wijWnkgBvDPyyNAa2ZcYYhkI9ohtYB6wn2kebwtEJ6sOGd1N8n rBzpz5MVEWHimwbWctQEJnOr56ckMfh_X8OSqjgclyUeeCyM5U bOOG2dSlcNKg335mCFF37QenNfG_gWd_HA-dWndwL0hr7R1TOznRlDrtmM6xtRC4x2gWGFrPL4_p0vKasW05v yriXwYW_kAoTAXUQYk7G8dqXpsmv1REz-8bSkxGiYVKln9vFTZR-OTfUSX69od9jJym7zxaQ3UkSanzcMatSo7ovGZTQ2-YUHu9qFMIAJTpKqEPIrR1zVpnj-Mqn97vFjstf4_kvnQyDmnJCppEFZBqUbIV4FA7ydg-EyOnKFHGfiawmmESj7VX6GYPWnPV1-G1uTVF3Rtv4PXtA5SYl_zRX5H-55j238wEO_wJ6Sz9M_rUUqtNcx1fLrVnQG_emPwyWhBykadcAD Zd4qPgwavPDn6HMJO8phkUFDP3wWd12IO2K-DpCDUIAA&state=1w42JUXSFcPhrzdJaV9Dvqhvv1lYt6c54p5y&session_state=b3b9afcf-d9ef-4a5f-9891-d25db5003526"


Then break that down in the text editor so you can see all the parts clearly (I have annotated this one a bit):


http: <--Protocol
//localhost <--Host
/GoogleAPI181/MSCallback.html <--Path
?code=AQABAAIAAAAm-06blBE1TpVMil8KPQ411ocdZfH2nYgskb3A3XX52FN_LvA9tti 9oLBSAEvuOD2KcjnWt4G1hoHXIqem8lVikMUnPlTKZKmqCHF_H wijWnkgBvDPyyNAa2ZcYYhkI9ohtYB6wn2kebwtEJ6sOGd1N8n rBzpz5MVEWHimwbWctQEJnOr56ckMfh_X8OSqjgclyUeeCyM5U bOOG2dSlcNKg335mCFF37QenNfG_gWd_HA-dWndwL0hr7R1TOznRlDrtmM6xtRC4x2gWGFrPL4_p0vKasW05v yriXwYW_kAoTAXUQYk7G8dqXpsmv1REz-8bSkxGiYVKln9vFTZR-OTfUSX69od9jJym7zxaQ3UkSanzcMatSo7ovGZTQ2-YUHu9qFMIAJTpKqEPIrR1zVpnj-Mqn97vFjstf4_kvnQyDmnJCppEFZBqUbIV4FA7ydg-EyOnKFHGfiawmmESj7VX6GYPWnPV1-G1uTVF3Rtv4PXtA5SYl_zRX5H-55j238wEO_wJ6Sz9M_rUUqtNcx1fLrVnQG_emPwyWhBykadcAD Zd4qPgwavPDn6HMJO8phkUFDP3wWd12IO2K-DpCDUIAA <--Code
&state=1w42JUXSFcPhrzdJaV9Dvqhvv1lYt6c54p5y <--State
&session_state=b3b9afcf-d9ef-4a5f-9891-d25db5003526 <--Session State


So the long one in there is (or should be) the code.

Back in the browser's debugger, step on through until you have stepped over the line:


code = obj.queryValue(url, obj.wpsAuthCdName);


This is what should get passed back into the DataFlex in the web property wpsAuthCode - check that it is (in the DataFlex debugger).

If you get that far, get back to me here.

Mike

SEHS
28-May-2020, 12:19 PM
It seems correct in the debugger.


13725

Mike Peat
28-May-2020, 12:48 PM
OK, so the callback page is getting called and the code looks right (well, I have no idea what it should look like, but I'll take your word ;)).

Is that code turning up in the DataFlex side of the oOAuth2?

You might follow the procedure I set out above a bit further in the JavaScript and see if it steps through into the final part of the login function:


else {
obj.set("wpsAuthCode", code, false);
obj.set("wpiExpiresIn", expires, false);
obj.set("wpbLoggedIn", true, false);
obj.set("wpsErrorCode", "", false);
obj.set("wpsErrorDesc", "", false);
obj.serverAction("LoginDone");
}


And actually executes the obj.serverAction("LoginDone"); bit, or debugging in the DataFlex to see if it gets into the LoginDone procedure in the cOAuth2 class.

If it gets that far, then the problem will most likely be with the exchange of the authorization code for the actual access token that you use to make your calls to the service.

Mike

SEHS
28-May-2020, 01:27 PM
Well, it does not execute "LoginDone".

I get this: "Returned state does not match passed state: possible attempted Cross Site Request Forgery attack", so I am thinking that there might be something wrong in the AddParam's in OnBeforeLogin.

Here is what it should look like:


https://identity-sandbox.test.vismaonline.com/connect/authorize
?client_id={ClientID}
&redirect_uri=https://domain.com/app/OAuth2/Callback.html
&scope=ea:api%20offline_access%20ea:sales%20ea:purc hase_readonly%20ea:accounting
&state=018TEST7643
&response_type=code
&prompt=login
&acr_values=service:01234AB1-1A23-1A2B-A123-123AB4567890+forceselectcompany:true


This is how I set it up in DF code:


Procedure OnBeforeLogin
Send ClearParams
Send AddParam "scope" "ea:api%20offline_access%20ea:sales%20ea:purchase_r eadonly"
Send AddParam "state" "018TEST7643"
Send AddParam "prompt" "login"
Send AddParam "acr_values" "service:01234AB1-1A23-1A2B-A123-123AB4567890+forceselectcompany:true"
End_Procedure

Mike Peat
29-May-2020, 02:20 AM
OK, so there is your problem. The "state" parameter is designed to alert you to a possible CSRF attack: the returned value should match the passed value - basically the service should return the value you send to it (don't ask me how this protection actually works - I've never figured it out, but there we have it <g>).

Try removing the Send AddParam "state" from your OnBeforeLogin procedure in the DF code.

Rightly or wrongly (perhaps reflecting the fact that I clearly didn't really understand the issue <g>), the OAuth2 JavaScript component assigns the value of your DataFlex session cookie to the state parameter, but you are supplying an additional "state" parameter, which is confusing the service. The service is obviously returning the latter of these two, which does not match the one the component is expecting, which is what is going wrong. Just let the component do its thing (i.e. DO NOT supply that additional state parameter) and it should be OK I think.

Mike

Mike Peat
29-May-2020, 02:33 AM
Addendum: you can read about the use of the "state" parameter here: https://auth0.com/docs/protocols/oauth2/oauth-state.

Reading that I realise that the DF session cookie value probably isn't really the best choice - it should probably generate it's own random value and use that - but basically it works and since the cookie value is itself generated (reasonably) randomly this is (I believe) an "if it ain't broken, don't fix it" type of situation.

Others more knowledgeable about such stuff may disagree. ;) (In which case let me know and I will look at fixing it.)

Mike

SEHS
29-May-2020, 07:28 AM
Works perfectly now, thanks! :)

Mike Peat
29-May-2020, 09:13 AM
Hth :)

Roel Westhoff [W4]
20-Jul-2020, 01:28 AM
Good morning Sehs,

I've been looking at this thread and it gives some good insights how to connect to Visma, a well know software supplier in the nothern part of Europe (scandinavia and netherlands)

Is it possible for you to put your findings on oAuth2 and Visma in a blog?

For future generations of Dataflex developers.

tia
Roel

Mike Peat
20-Jul-2020, 06:01 AM
"future generations of Dataflex developers"... have you got a breeding program going Roel? ;)

Albin
8-Sep-2020, 09:33 AM
OK, here is what I'd do...

With your app open in the browser (in debug-run from the DF Studio), but before you attempt to log in to the service, right-click somewhere on the browser page and from the pop-up menu select "Inspect" (in Chrome) or "Inspect Element (Q)" (in Firefox), then in the window that opens up go to the "Sources" tab (in Chrome) or the "Debugger" tab (in Firefox). In the left-hand pane of that, navigate to the oOAuth2.js file.

Set a breakpoint (click the line number) just after the "url = " line:


var pollTimer = window.setInterval(function() {

try {

if (win.document.URL.indexOf(obj.wpsRedirectUrl) != -1) {
window.clearInterval(pollTimer);
url = win.document.URL;
win.close(); // <-- set the breakpoint here



(About line 81 or 82 in my version.)

Then go through your login procedure (click the button or whatever) and perform the login - you should then hit the breakpoint in the browser's debugger.

Look at what is in that url variable (hovering over it will generally show you, but find it in the Scopes pane of the browser's debugger which might make it easier you to copy it's content). Then paste that into a text editor for examination:

It will be something like this example from Google:


"http://localhost/GoogleAPI181/MSCallback.html?code=AQABAAIAAAAm-06blBE1TpVMil8KPQ411ocdZfH2nYgskb3A3XX52FN_LvA9tti 9oLBSAEvuOD2KcjnWt4G1hoHXIqem8lVikMUnPlTKZKmqCHF_H wijWnkgBvDPyyNAa2ZcYYhkI9ohtYB6wn2kebwtEJ6sOGd1N8n rBzpz5MVEWHimwbWctQEJnOr56ckMfh_X8OSqjgclyUeeCyM5U bOOG2dSlcNKg335mCFF37QenNfG_gWd_HA-dWndwL0hr7R1TOznRlDrtmM6xtRC4x2gWGFrPL4_p0vKasW05v yriXwYW_kAoTAXUQYk7G8dqXpsmv1REz-8bSkxGiYVKln9vFTZR-OTfUSX69od9jJym7zxaQ3UkSanzcMatSo7ovGZTQ2-YUHu9qFMIAJTpKqEPIrR1zVpnj-Mqn97vFjstf4_kvnQyDmnJCppEFZBqUbIV4FA7ydg-EyOnKFHGfiawmmESj7VX6GYPWnPV1-G1uTVF3Rtv4PXtA5SYl_zRX5H-55j238wEO_wJ6Sz9M_rUUqtNcx1fLrVnQG_emPwyWhBykadcAD Zd4qPgwavPDn6HMJO8phkUFDP3wWd12IO2K-DpCDUIAA&state=1w42JUXSFcPhrzdJaV9Dvqhvv1lYt6c54p5y&session_state=b3b9afcf-d9ef-4a5f-9891-d25db5003526"


Then break that down in the text editor so you can see all the parts clearly (I have annotated this one a bit):


http: <--Protocol
//localhost <--Host
/GoogleAPI181/MSCallback.html <--Path
?code=AQABAAIAAAAm-06blBE1TpVMil8KPQ411ocdZfH2nYgskb3A3XX52FN_LvA9tti 9oLBSAEvuOD2KcjnWt4G1hoHXIqem8lVikMUnPlTKZKmqCHF_H wijWnkgBvDPyyNAa2ZcYYhkI9ohtYB6wn2kebwtEJ6sOGd1N8n rBzpz5MVEWHimwbWctQEJnOr56ckMfh_X8OSqjgclyUeeCyM5U bOOG2dSlcNKg335mCFF37QenNfG_gWd_HA-dWndwL0hr7R1TOznRlDrtmM6xtRC4x2gWGFrPL4_p0vKasW05v yriXwYW_kAoTAXUQYk7G8dqXpsmv1REz-8bSkxGiYVKln9vFTZR-OTfUSX69od9jJym7zxaQ3UkSanzcMatSo7ovGZTQ2-YUHu9qFMIAJTpKqEPIrR1zVpnj-Mqn97vFjstf4_kvnQyDmnJCppEFZBqUbIV4FA7ydg-EyOnKFHGfiawmmESj7VX6GYPWnPV1-G1uTVF3Rtv4PXtA5SYl_zRX5H-55j238wEO_wJ6Sz9M_rUUqtNcx1fLrVnQG_emPwyWhBykadcAD Zd4qPgwavPDn6HMJO8phkUFDP3wWd12IO2K-DpCDUIAA <--Code
&state=1w42JUXSFcPhrzdJaV9Dvqhvv1lYt6c54p5y <--State
&session_state=b3b9afcf-d9ef-4a5f-9891-d25db5003526 <--Session State


So the long one in there is (or should be) the code.

Back in the browser's debugger, step on through until you have stepped over the line:


code = obj.queryValue(url, obj.wpsAuthCdName);


This is what should get passed back into the DataFlex in the web property wpsAuthCode - check that it is (in the DataFlex debugger).

If you get that far, get back to me here.

Mike

Hi Mike.

Iīm playing around with this to authenticate with Microsoft Graph API and got stuck right here.
I do not seem to get past this js line (79):

if (win.document.URL.indexOf(obj.wpsRedirectUrl) != -1) {
Is this checking if the URL is containing my redirectURL?
The popup windows comes up and I get redirected to my RedirectURL and looking at the URL it does contain a "code" at the end.
But since I do not get past this line DF does nothing after this. Debugging the JS I can see that the timer hits again and again but always stops at this line.

Mike Peat
8-Sep-2020, 09:48 AM
Albin

The idea is that the JavaScript above will just keep trying until it is able to open the document. That needs to be in exactly the same domain as your WebApp. Is it?

Mike

Albin
8-Sep-2020, 09:56 AM
Albin

The idea is that the JavaScript above will just keep trying until it is able to open the document. That needs to be in exactly the same domain as your WebApp. Is it?

Mike

Ok, no it is not. The javascript is but the Redirect website is published on another server. That is because Microsoft Graph needs to have the redirect URL registered in our Azure. If it isīnt we will not get a valid client ID/Secret.
So how to solve that one..

Mike Peat
9-Sep-2020, 02:44 AM
Albin

Sorry, I don't know the answer to that. The OAuth2 rules are clear: the requesting JavaScript and the Redirect URL must be in the same domain. :(

You might be able to fudge around that by having them in the same xxx.company.tld and fiddling with that, but I can't exactly remember how to do that off the top of my head.

Mike

Albin
10-Sep-2020, 01:05 AM
Albin

Sorry, I don't know the answer to that. The OAuth2 rules are clear: the requesting JavaScript and the Redirect URL must be in the same domain. :(

You might be able to fudge around that by having them in the same xxx.company.tld and fiddling with that, but I can't exactly remember how to do that off the top of my head.

Mike

Thanks for all your help Mike!
We got to the conclusion that for windows when using ActiveX Webbrowser this is not a problem.
When using Iframe it is, so for customers with their own server that is using web we will have to publish a redirect page there.