softwerkab / fortnox-csharp-api-sdk

.NET SDK for Fortnox API.
MIT License
52 stars 64 forks source link

.Net SDK and new Authorization Code Flow #159

Closed bmjohanp closed 3 years ago

bmjohanp commented 3 years ago

Hi,

Does the .Net SDK support the short lived acces tokens produced with the new Authorization Code Flow? https://developer.fortnox.se/general/authentication/

I'm getting a "Could not login, access token or client secret is missing" message when running SDK 4.1.0, even though both are set in FortnoxClient.

bmjohanp commented 3 years ago

Well it seems the answer is "no" the sdk does not support Authorization Code Flow. I had to use a workaround where I provide an HttpClient to FortnoxClient.

httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken); var client = new FortnoxClient("", "", httpClient);

richardrandak commented 3 years ago

Hi, I am glad you found a workaround. Since I don't have access to any published app, I was not able to try the new workflow yet. I will check this again. This should be supported in the SDK, so I will implement it soon.

richardrandak commented 3 years ago

Hello @johanpe I created a preview for a NuGet (v4.2.0-alpha) with support for both auth workflows. Since I don't have any published app, could you try with yours? I have wrapped the credentials into an authorization objects in order to select the the workflow you need.

StaticTokenAuth -> old one, using AccessToken and ClientSecret StandardAuth -> new one, following the OAuth2 standard, using short-lived AccessToken

//old one (unpublished apps)
new FortnoxClient(new StaticTokenAuth(accessToken, clientSecret));
// new one (published apps)
new FortnoxClient(new StandardAuth(accessToken));```

Are you using some kind of OAuth library to handle the new auth workflow? I have added some utility methods you could use in FortnoxAuthClient. It allows to build the login uri, get access token or do the token refresh. I would be glad if you could confirm if they work or not

// old one (unpublished apps)
var authClient = new FortnoxAuthClient();
var authWorkflow = authClient.StaticTokenAuthWorkflow;
var accessToken = authWorkflow.GetToken(authCode, clientSecret);
//new one (published apps)
var authClient = new FortnoxAuthClient();
var authWorkflow = authClient.StandardAuthWorkflow;

var authUri = authWorkflow.BuildAuthUri(clientId, scopes, state, redirectUri);

var tokenInfo = authWorkflow.GetToken(authCode, clientId, clientSecret, redirectUri);
var accessToken = tokenInfo.AccessToken;
var refreshToken = tokenInfo.RefreshToken;

var refreshedTokenInfo = authWorkflow.RefreshToken(refreshToken);
xpagedeveloper commented 3 years ago

The first step to Authorize the API and get accesstoken and refreshtoken works but when you want to get a new accesstoken from the refreshtoken, that part seams to miss something.

Refresh Access-Token https://developer.fortnox.se/general/authentication/

This is the code I have tried var authClient = new FortnoxAuthClient(); var authWorkflow = authClient.StandardAuthWorkflow; var refreshedTokenInfo = authWorkflow.RefreshToken(refreshToken);

I guess we are missing that the API requires Clientid & ClientSecret being sent as an basic Authorization

richardrandak commented 3 years ago

You are right! Thank you, I missed that. I have added the authorization header to the refresh token request now. Can you try now? NuGet -> https://www.nuget.org/packages/Fortnox.NET.SDK/4.2.0-beta

xpagedeveloper commented 3 years ago

Yes it works :-)

xpagedeveloper commented 3 years ago

Also works with the old way with a fixed accesstoken

bmjohanp commented 3 years ago

Thank you for the beta, the new utility methods and the changes to FortnoxClient are nice. One comment though:

The StandardAuthWorkflow.RefreshTokenAsync method doesn't have any error handling, it is using HttpClient.SendAsync instead of BaseClient.SendAsync. This results in RefreshTokenAsync returning invalid TokenInfo even though we get an http error code from the api.

richardrandak commented 3 years ago

Ah, right.. thank you for the feedback When I get some time, I will add the handler for the standard OAuth error responses.. hopefully the fortnox auth server follows them :)

bmjohanp commented 3 years ago

StandardAuthWorkflow.GetTokenAsync() use BaseClient.SendAsync() to send the http request in the beta, so I just assumed that StandardAuthWorkflow.RefreshTokenAsync() should do the same, instead of using HttpClient.SendAsync().

Another issue with the new StandardAuthWorkflow:

RefreshTokenAsync() and GetTokenAsync() call "await" without using ConfigureAwait(false). This is causing a deadlock in certain situations in our asp.net mvc app.

richardrandak commented 3 years ago

Hah, that is embarrassing.. I guess I have done it in a rush :D Thank you very much for this. I'll check it out.

richardrandak commented 3 years ago

@johanpe Should be fixed now.. you can check the NuGet v4.2.0-rc

bmjohanp commented 3 years ago

@richardrandak Works great now, thank you!

niklaswulff commented 3 years ago

When I'm using StandardAuthWorkFlow I can't use the apps Authorization Code, and when I try the StaticAuthWorkflow, I can create an access token, but when I use that to query for customer I get an error.

The Fortnox app isn't published, and possibly won't ever be.

In the above description of the Standard flow, you start with requesting a URI: var authUri = authWorkflow.BuildAuthUri(clientId, scopes, state, redirectUri);

I don't see the use of this, is this something that I have to do? Since I already have the Authorization Code?

bmjohanp commented 3 years ago

@niklaswulff Is the integration recently activated, is it a newly created authorization code? The reason I ask is because we have had lots of problems with the new authorization code flow due to the fact that Fortnox have broken compatability and haven't documented the changes very well. If you activate an integration in the Fortnox portal now, the authorization code can be used to generate an access token, but the token can't actually be used. All integrations that are activated now must use the new authorization code flow, regardless of whether the app is published or not. Our app is not published, but after spending a LOT of time with the support we found out that this is the case. So we had to implement the new authorization code flow to get the api to work again, and that is why i detected that the .Net sdk didn't support the new flow and created this issue.

More info regarding the new auth. code flow: https://developer.fortnox.se/general/authentication/

niklaswulff commented 3 years ago

@johanpe It's brand new (as I am to this API), so the authorization code is created now. Since the activation code expire fast, I've been adding the same app multiple times.

I tried the Standard flow now, and got this error message from the SDK: Authorization code doesn't exist or is invalid for the client

I double checked the auth code and the client info of course.

This is the code:

        var authorizationCode = SystemSettingValue.FortnoxAuthorizationCode;

        var authWorkflow = new FortnoxAuthClient().StandardAuthWorkflow;

        var tokenInfo = authWorkflow.GetToken(authorizationCode, ClientId, ClientSecret); <--- ERROR HERE
        var refreshToken = tokenInfo.RefreshToken;

        return tokenInfo.AccessToken;
richardrandak commented 3 years ago

Hi @niklaswulff !

I checked just now, i created a new app and the old (static token) workflow still works for me (at least with sandbox). If you got your authorization code by Fortnox portal in Manage Users->Add Integration, then you should use this "static token" workflow.

            // Activate
            var clientSecret = "CANrpxMPfv";
            var authorizationCode = "51af752d-6c62-37eb-aeb3-96a5b0656e3d";

            var fortnoxAuthClient = new FortnoxAuthClient();
            var authWorkflow = fortnoxAuthClient.StaticTokenAuthWorkflow;

            var token = authWorkflow.GetToken(authorizationCode, clientSecret);

            //Use
            var authorization = new StaticTokenAuth(token, clientSecret);
            var fortnoxClient = new FortnoxClient(authorization);

            var query = new CustomerSearch()
            {
                // Search parameters
            };
            var collection = fortnoxClient.CustomerConnector.Find(query);

If you already have the access token, just skip the "activate" part and just do the "use" part, since in this workflow, the access token is retrieved only once and is valid forever.

In my experience, the "authorization code" retrieved this way can't be used with the new OAuth "standard" auth workflow, and results with the error you mentioned.

richardrandak commented 3 years ago

Also, the authorization code retrieved by the old way (through Add Integration) is valid for many days, so you don't have to be worried about expiration.

The documentation of Fortnox "Getting Started" and "Authorization" is bad.. when they introduced the new workflow, they mixed it up with the old one, but in reality, they can't be mixed. Either you use it the old way, or you use it the new way.

bmjohanp commented 3 years ago

@richardrandak Strange, the old static way didn't work for us for new integrations, only for existing ones. And according to Fortnox support we had to use the new flow for new integrations. They also confirmed that the old way will be end of life in december, which gave us no choice but to implement the new flow before releasing the app.

From a Fortnox news email in june:

"2021-12-09: End of life for the existing authorization flow with long-lived access tokens. All integrations must use the OAuth2 Authorization Code Flow with expiring access tokens."

I must say that the documentation and support from Fortnox isn't always top notch...

richardrandak commented 3 years ago

I guess we won't know until the 2021-12-09 comes :D

From my experiments, it seems that even unreleased apps can use the new workflow now, but the key point is that you can't mixed them. I think the most confusing thing is that Step 4 in Getting Started guide explains how to get Authorization Code by the old way, while Step 5 explains how to exchange it for AccessToken by the new way. This, however, does not work. Seems like there are two types of authorization codes, even though they are both in GUID format.

niklaswulff commented 3 years ago

@richardrandak and @johanpe Thanks a bunch for trying to clarify!

Since Johan is adamant of the expiration of the static flow, I'll go with the new way. However, when I tried using it I encountered the error I described above. Am I missing something? I used the auth code from the "add integration" UI, is there another way to get an auth code?

richardrandak commented 3 years ago

Yes, the new way to get the auth code is following the OAuth2 workflow, common for other REST API integrations. it will basically be sent you the code by a HTTP callback, so you need a simple web server. You can try with localhost and have to configure the Redirect URL in your Fortnox App to match it

niklaswulff commented 3 years ago

There's something in OAuth2 flow that I don't get - what would be the use of redirectUrl?

We are doing an integration that will query Fortnox every night to update data in our customers instance of our web app. So I don't want any user interaction. In the example on Fortnox documentation they have the user interacting in the auth flow, but I don't get how that would work.

I'm sorry if I seem slow, but I think it's hard to wrap my head around this.

bmjohanp commented 3 years ago

@niklaswulff If you use the new flow, you would have to have an activation process in your app that involves interaction:

1) Forward the user to Fortnox, using BuildAuthUri(). Provide the callback url that you want Fortnox to redirect the user to after activation (redierctUrl). The user logins and approves the integration. 2) Handle the authorization code in the callback from Fortnox, and fetch initial tokens (access + refresh). Store these for your backend to use.

richardrandak commented 3 years ago

No worries, I had a hard time understanding the human interaction too, since I haven't been a web developer before..

I recommend you to use the old workflow which is perfect for your use case..

In case of OAuth you need the human interaction at first time.. then you obtain a refresh token together with the access token. This can be used to automatically obtain the access token again and again so that you have always the token up-to-date without requiring the human every single time. I don't want to try explaining the OAuth through GitHub forum so if you want to know more please read about it first :)

niklaswulff commented 3 years ago

@johanpe Great summary, thanks! Although I'm quite surprised that it's impossible to do it only M2M.

@richardrandak haha, I appreciate this extent of support and wasn't expecting an OAuth2 primer, ;-)

niklaswulff commented 3 years ago

@richardrandak And regarding using the old workflow - I tried it and couldn't get the access token to work unfortunately. I have requested support from Fortnox and am eagerly awaiting their call.

bmjohanp commented 3 years ago

We had the exact same problem, the access tokens for the old workflow dindn't work, so we implemented the new workflow. I'm interested in hearing the response you get from support.

niklaswulff commented 3 years ago

I would also be interested in seeing relevant parts of your code based on the new workflow - I only get an error...

bmjohanp commented 3 years ago

I think you might need to activate the oauth2 code flow for the app in the developer portal, but I'm not sure.

When activating the integration in your app (webb app admin page, etc), redirect the user to Fortnox. To get the url to redirect to:

var scopes = new Scope[] { ... } var authClient = new FortnoxAuthClient(); var url = authClient.StandardAuthWorkflow.BuildAuthUri(appClientId, scopes , "myrandomstatestring", redirectUrl);

When you handle the auth. code in the callback (redirectUrl) you request tokens using the auth. code provided by Fortnox:

var authClient = new FortnoxAuthClient(); var tokenInfo = authClient.StandardAuthWorkflow.GetToken(authCode, appClientId, appClientSecret)

Store the token info and use the access token whith each api call.

fortnoxApiClient = new FortnoxClient(); fortnoxApiClient.Authorization = new StandardAuth(myCurrentAccessToken);

Use the refresh token to refresh the access token when needed (access token has expiration, see TokenInfo.ExpiresIn). Remember to also store the new refresh token that is returned in TokenInfo when refreshing the access token.

var authClient = new FortnoxAuthClient(); var tokenInfo = authClient.StandardAuthWorkflow.RefreshToken(refreshToken, appClientId, appClientSecret);

niklaswulff commented 3 years ago

@johanpe Thanks, much appreciated!

richardrandak commented 3 years ago

@niklaswulff any news from official Fortnox support?

niklaswulff commented 3 years ago

@richardrandak yes, I got the reply yesterday, and key points are:

We have launced our first integration with one customer, but since no new connections will work after december, we will soon start to use OAuth2 instead for future activations with other customers.

richardrandak commented 3 years ago

Thanks, this is what I expected. So.. what was the problem with your static workflow before? You said you could not make it work.

niklaswulff commented 3 years ago

@richardrandak It was error code 40 (ie my fault) unfortunately. :-) I failed to persist the access token correctly.

niklaswulff commented 3 years ago

@richardrandak I'm setting up the OAuth flow now, but I can't find any documentation on the callback/redirecturl that Fortnox will redirect my user to after authing in Fortnox. How will that look? GET or POST? I see the "state" parameter for instance, will this be in the query string or form body? I haven't found any details at all.

richardrandak commented 3 years ago

The URL is the one you set up in the app's configuration (in your fortnox dev portal). It is a GET request with "code" and "state" as URL parameters.

bmjohanp commented 3 years ago

You can also leave the url empty in the dev portal, and only specify it when you build the auth. url:

StandardAuthWorkflow.BuildAuthUri(myClientId, myScopes, myState, redirectUrl)

Our app for example is hosted on different domains depending on the client, so we must have customized callback urls for each instance.

niklaswulff commented 3 years ago

@johanpe Great, thanks! I reacted to that reading @richardrandak 's answer, because we also have different domains per customer...

@richardrandak thanks, is this documented somewhere that I missed?

bmjohanp commented 3 years ago

https://developer.fortnox.se/general/authentication/ Under Request examples - Response redirect

You can also get "error_code" and "error_description" as url parameters in case of an error, but it is not documented.

niklaswulff commented 3 years ago

You can also get "error_code" and "error_description" as url parameters in case of an error, but it is not documented.

Great!

xpagedeveloper commented 3 years ago

You can’t change the redirect url in a oauth flow. from the fortnox dev docs ”URL-encoded URI that must match the Redirect URI for the app set in the Developer Portal. If omitted, it will default to the registered Redirect URI.” Any redirect that is custom for each customer must be handled when you get the result back perhaps using the state param.

bmjohanp commented 3 years ago

I can confirm that it does indeed work to leave the redirect uri empty in the portal and specify it in the authorization url, since we are using it this way. When we implemented this in july we also got an ok from the support saying that "It shouldn't be a problem" handling it this way.

This wouldn't be the first time the Fortnox documentation (or support) is incomplete or false I'm afraid. But I hope they don't change the behaviour, because not being able to use custom redirect uri:s would make things a lot harder for us.

Also, I think it is common practice in ouath flow to let the client set up multiple redirect uri:s and then specify one of them in the authorization call. But I could be wrong here? So if Fortnox decides to change the current procedure in any way I hope they will at least let us set upp multiple redirect uri:s in the portal.

xpagedeveloper commented 3 years ago

That is great news because I would need it to work that way also. 😃

richardrandak commented 3 years ago

Huh.. I think it is pretty standard to have just one (or limited number of configured redirect URIs). I am not a web developer, but I feel that to handle redirects based on customer type, it should be done after the OAuth, maybe by cookies? Using the "state" is also an option, but i think that it has a different purpose.

Before starting with Fortnox auth, you save the customer type to cookies. The Fortnox redirects user to a predefined uri, on which the access token is obtained automatically, and then you can redirect the user based on the customer type stored in the cookie. The redirection logic based on customer is then flexible and not dependent on Fortnox auth workflow.

In the end, it means user is redirected two times, but it won't be noticed, since it will be automatic and smooth.

EDIT: or you can obtain the access token after the customer-type redirection logic, if you have some special token exchange/storage based on customer type. All I mean is, that customer type redirection should be done after oauth redirect logic :)

bmjohanp commented 3 years ago

@richardrandak Not really sure I follow the flow you are decribing, and if it would work in our scenario. It's not just a "customer type" thing, our systems are runnig on different serverns, different web servers, different databaser servers, and so on. So some of the systems ar hosted by us, on different servers, some are running on the customers own servers (self hosted, closed system). So I'm not really sure this would work with a single rediect uri. Unless we create a Fortnox-app instance for each customer system instance.

richardrandak commented 3 years ago

Has anybody else encountered a problem when your app is not visible from client's account? I mean, when you want to add integration, you search it by ClientId, and the app should appear so you can get the API-code.

Suddenly, it stopped showing up. I created a new one, it was showing up, but after few hours I tried and it also not available anymore.

richardrandak commented 3 years ago

Topic #187 was added for further discussion,