auth0 / auth0-oidc-client-net

OIDC Client for .NET Desktop and Mobile applications
https://auth0.github.io/auth0-oidc-client-net/
Apache License 2.0
86 stars 49 forks source link

Can't refresh token with scopes #218

Closed lubiepomaranczki closed 2 years ago

lubiepomaranczki commented 2 years ago

Describe the problem

When Refreshing a token from Xamarin.Forms app, on Auth0 side we don't get scopes. This is important to us, as we have custom scope and based on it we are adding custom claims. When requesting the token from Postman or by HttpClient, refreshing token works. We tried several cases, so let me write those here.

  1. Working out of the box - does not work Firstly we tried really simple approach (all code is from Xamarin.iOS platform).

    public async Task<IdentityModel.OidcClient.LoginResult> LoginAsync()
    {
    var auth0Client = new Auth0Client(new Auth0ClientOptions
    {
        Domain = _appSettings.Auth0_DomainName,
        ClientId = _appSettings.Auth0_ClientId,
        Browser = new SFSafariViewControllerBrowser(),
        Scope = "openid profile customscope email offline_access",
    });
    
    var options = new { audience = _appSettings.Auth0_Audience};
    var result = await auth0Client.LoginAsync(extraParameters: options);
    
    var newAccessToken = await auth0Client.RefreshTokenAsync(result.RefreshToken, options);
    
    return result;
    }

    With this approach, access token in result was correct. It has all needed claims, what's more, refresh token was also there. Unfortunately, when making RefreshTokenAsync, on Auth0 side the call was missing scopes, hence accessToken received in newAccessToken was useless - it didn't have needed claims.

  2. Let's push scopes in optional parameters - didn't work too

    public async Task<IdentityModel.OidcClient.LoginResult> LoginAsync()
    {
    var auth0Client = new Auth0Client(new Auth0ClientOptions
    {
        Domain = _appSettings.Auth0_DomainName,
        ClientId = _appSettings.Auth0_ClientId,
        Browser = new SFSafariViewControllerBrowser(),
        Scope = "openid profile customscope email offline_access",
    });
    
    var options = new { audience = _appSettings.Auth0_Audience};
    var result = await auth0Client.LoginAsync(extraParameters: options);
    
    var newAuth0Client = new Auth0Client(new Auth0ClientOptions
    {
        Domain = _appSettings.Auth0_DomainName,
        ClientId = _appSettings.Auth0_ClientId,
        Browser = new SFSafariViewControllerBrowser(),
    });
    
    var newOptions = new
    {
        audience = _appSettings.Auth0_Audience,
        scope = "openid profile customscope email offline_access"
    };
    var newAccessToken = await newAuth0Client.RefreshTokenAsync(result.RefreshToken, newOptions);
    
    return result;
    }

    Within the second approach we decided to get rid of scopes from Auth0Client and pass them in options. With this approach the call was stuck on line with RefreshTokenAsync. Does not matter how long we waited - it was running forever.

This leads to conclusion that we believe something is wrong with sending scopes in the library.

  1. Using HttpClient to refresh token

With the last approach, we tried to refresh token from Postman and it worked. We agreed that for the time being we will refresh token using HttpClient

This code normally works

public async Task<RefreshTokenRequest?> RefreshToken(string refreshToken)
{
    using var client = new HttpClient
    {
        BaseAddress = new Uri($"https://{_appSettings.Auth0_DomainName}"),
    };
    {
        var dict = new Dictionary<string, string>();

        dict.Add("grant_type", "refresh_token");
        dict.Add("client_id", _appSettings.Auth0_ClientId);
        dict.Add("refresh_token", refreshToken);
        dict.Add("scope", _scopes);
        dict.Add("audience", _appSettings.Auth0_Audience);

        var req = new HttpRequestMessage(HttpMethod.Post, "/oauth/token") { Content = new FormUrlEncodedContent(dict) };
        var res = await client.SendAsync(req);

        var result = await res.Content.ReadAsStringAsync();
        if (res.IsSuccessStatusCode)
        {
            return JsonConvert.DeserializeObject<RefreshTokenRequest?>(result);
        }
    }

    return null;
}

What was the expected behavior?

Obviously we would like to get refresh token from the library but somehow it does not work.

Environment

Auth0.OidcClient version 3.2.4 with Xamarin

Let me know if you need any help! Thanks! 🙏

frederikprijck commented 2 years ago

Hello,

I have been unable to reproduce this. I have used the following code:

var _auth0Client = new Auth0Client(new Auth0ClientOptions
            {
                Domain = "{DOMAIN}.auth0.com",
                ClientId = "{CLIENT_ID}",
                Scope = "openid profile Scope1 email offline_access",
                Browser = new SFSafariViewControllerBrowser(),
            });

var options = new {audience = "Test"};
var loginResult = await _auth0Client.LoginAsync(options );
var refreshTokenResult = await _auth0Client.RefreshTokenAsync(loginResult.RefreshToken, options);

Then I used jwt.io to compare both Access Tokens and I can clearly see that both have the correct scope: "scope": "openid profile email Scope1 offline_access".

There has to be something else going on. Do u have any rules or actions enabled ?

arrivant commented 2 years ago

Yes, we have multiple custom rules enabled in our pipeline. The issue is that input to the first rule in the pipeline doesn't have scope property at all and since we have some conditions based on the existence of it, we are not able to get all the claims that we need.

frederikprijck commented 2 years ago

Could you try if you can reproduce this with the rules disabled? That will allow us to exclude any of the rules is causing this.

lubiepomaranczki commented 2 years ago

... but we can easily refresh the token with POST call via HttpClient (see above). Is there any chance that rules have something to do with this package?

frederikprijck commented 2 years ago

It is always a good idea to troubleshoot without rules in situations like this.

I have been trying to reproduce this but haven't been able to do so. That's why anything that can help pinpoint this can be useful.

I am also curious to know what RefreshTokenRequest is in your code, would you mind sharing that?

lubiepomaranczki commented 2 years ago

As far as I rememberRefreshTokenRequest comes from IdentityModel.Client package 😇

Not sure if we can try to run it with Rules disabled... @arrivant do you think we could do that?

frederikprijck commented 2 years ago

What I am trying to understand is how that code you posted works when u expect the response to be of type RefreshTokenRequest. Is that perhaps a typo or is that actually intended?

It would be very helpful if you could take our sample application (https://github.com/auth0-samples/auth0-xamarin-oidc-samples/tree/master/Quickstart/01-Login), add the code to use refresh tokens and see if u can reproduce it (with and without rules). If you can reproduce it, would you mind sharing the changes (or fork the repo and share a link to a branch that includes your changes)?

Could also be helpful if you could add the working code using HttpClient in there as well.

frederikprijck commented 2 years ago

Closing this as it's inactive. Feel free to reply and we can re-open if you still need assistance!

lubiepomaranczki commented 1 year ago

I was playing a bit with this. Are you sure @frederikprijck that you get same scopes in

   var loginResult = await _auth0Client.LoginAsync(options );
   var refreshTokenResult = await _auth0Client.RefreshTokenAsync(loginResult.RefreshToken, options);

loginResult & refreshTokenResult?

To me, it looks like scopes are missing when refreshing the token.

What' more, this ticket sounds pretty relevant to this case https://github.com/auth0/auth0-oidc-client-net/issues/241

frederikprijck commented 1 year ago

Yes, what's pointed out in #241 is correct, but what's happening is the fact that using a refresh token does not need the scope argument, unless you want to de-scope the access token.

So if you initially request a set of tokens for scopes Aand B, the refresh token is bound to these scopes. Any time you use it, it will return access tokens with both scopes A and B, without sending any scopes. (this is what is being reported in this issue to not work as expected, which I can not reproduce with the SDK because it's caused by your rules)

What you can do with the scope parameter is try to descope the tokens, meaning u want to use the refresh token for A and B, to retrieve a token for only A or B. As long as you do not need that, there is no reason to send along scope.

What sometimes happens is people build their rules/extensions on the assumption that scope is always present (which appears to be case here) , but that's incorrect. If you need scope to be present when refreshing the token to execute certain logic, you are mis-using the purpose of scope. Instead you would need to set a different request parameter, one that isnt optional in the first place.

That said, if you do want to de-scope the access token when issuing the refresh token, that's not supported currently by this SDK, and called out in #241.

If the above would be resolved, you could use the scope parameter for this. But I would still recommend not to. Even more so, by not using the scope parameter, you should be able to achieve what you want to achieve perfectly fine in the current version of the SDK.