googleapis / google-api-dotnet-client

Google APIs Client Library for .NET
https://developers.google.com/api-client-library/dotnet
Apache License 2.0
1.36k stars 527 forks source link

Using AuthorizationCodeWebApp class in .NET8 WebApi #2856

Closed akordowski closed 1 month ago

akordowski commented 1 month ago

I would like to pick up on the issue #2826. I am trying for days to figure out how to use the AuthorizationCodeWebApp class in a .NET 8 WebApi. I couldn't find any example so far, so I really hope any one can help on this. Here the code I have so far:

[ApiController]
[Route("[controller]")]
public class YouTubeController : ControllerBase
{
    private readonly GoogleAuthorizationCodeFlow _flow;

    public YouTubeController()
    {
        var clientSecrets = new ClientSecrets
        {
            ClientId = "...",
            ClientSecret = "..."
        };

        _flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
        {
            ClientSecrets = clientSecrets,
            Scopes = new[] { YouTubeService.Scope.YoutubeReadonly },
            DataStore = new FileDataStore("YouTubeApi")
        });
    }

    [HttpGet(Name = "YouTube")]
    public async Task<IActionResult> Get(CancellationToken cancellationToken)
    {
        var authResult = await new AuthorizationCodeWebApp(
                _flow,
                "https://localhost:7187/YouTube/Redirect",
                "https://localhost:7187/YouTube/State")
            .AuthorizeAsync("user", cancellationToken);

        if (authResult.RedirectUri != null)
        {
            return Redirect(authResult.RedirectUri);
        }

        return Ok();
    }

    [HttpGet("Redirect")]
    public async Task<IActionResult> Redirect(
        [FromQuery] string code,
        [FromQuery] string state,
        [FromQuery] string scope,
        CancellationToken cancellationToken)
    {
        var tokenResponse = await _flow.ExchangeCodeForTokenAsync("user", code, "https://localhost:7187/YouTube/State", cancellationToken);

        return Ok();
    }

    [HttpPost("State")]
    public async Task<IActionResult> State(CancellationToken cancellationToken)
    {
        return Ok();
    }
}

The Redirect() method is called with https://localhost:7187/YouTube/Redirect?state=https://localhost:7187/YouTube/State00801791&code=...&scope=https://www.googleapis.com/auth/youtube.readonly. The state https://localhost:7187/YouTube/State00801791 is also stored in the DataStore. But I have no idea how to use it. I tried to call _flow.ExchangeCodeForTokenAsync() but it throws an exception with the message TokenResponseException: Error:"redirect_uri_mismatch", Description:"Bad Request", Uri:"". Is that event the way to go?

The API has a own authentication provider. The authorization on Google should be rather a delegate authorization just for the services to use, not the API itself.

Can anyone provide an example how to proceed or give an hint at least? Any help is much appreciated! Thanks in advance.

jskeet commented 1 month ago

@amanda-tarafa should be able to help you tomorrow.

amanda-tarafa commented 1 month ago

The redirect URI you pass to ExchangeCodeForTokenAsync needs to be allowlisted on your credentials page (where you obtained your client ID and secret). Usually, you can use the same you used as callback for obtaining the authorization code, in your case https://localhost:7187/YouTube/Redirect.

Once you have obtained a TokenResponse, you should redirect the user to the original URI they requested, where it was found they were not authenticated. In your case, that'd be the URI for the Get controller method. You can use state to pass this URI all the way down to authorization code exchange.

State is not a URI in your application that Google will redirect the user to. State is whatever you need it to be so you can propagate information during the authenticaiton process. As I said earlier, this may be used to pass the original URI the user requested and/or the user identifier, etc.

akordowski commented 1 month ago

@amanda-tarafa Thank you for your response. I will try it on weekend and give you a feedback.

akordowski commented 1 month ago

@amanda-tarafa I tested your suggestions and here are my findings.

The call of the ExchangeCodeForTokenAsync() method works only if I pass the same redirectUri as used in the AuthorizationCodeWebApp class (in my example https://localhost:7187/YouTube/Redirect), although both urls are allowlisted in the console. My guess is that the redirectUri is somehow encoded in the received code. Am I right?

I also noticed that the state value is altered. In my example I recieve in the state parameter of the Redirect() method the following value https://localhost:7187/YouTube/State00801791. According to the source code a randon string is added to the end of the state.

https://github.com/googleapis/google-api-dotnet-client/blob/a598168817954e055e770d257ca3d54575b596bc/Src/Support/Google.Apis.Auth/OAuth2/Web/AuthorizationCodeWebApp.cs#L113-L120

Is that intentional? When yes, why? And how can I obtain the original data? As the string is random length I can not just strip the end of the string.

Thank you for your help.

amanda-tarafa commented 1 month ago

The documentation is not specific about which URIs may be passed for code exchange beyond the fact that they need to be allowlisted for your client ID. But yes, it's usual to pass the same URI. I cannot say with certainty that's because the URI is encoded in the authorization code or that the Auth service can map the code with the URI, etc. nor that it will happen for all cases. Maybe the OAuth team can give you a better answer, you can find their support channels at the bottom of the documentation link above.

The random number added will always have AuthorizationCodeWebApp.StateRandomLength, that's what the "D" + StateRandomLength parameters is doing in

var random = new Random().Next(int.Parse(rndString)).ToString("D" + StateRandomLength);

This is a primitive means to allow you to match a code authorization request with it's specific callback, if you need to do so. There's a TODO in the code to allow deactivating the addition of this random number, we might look into that at some point, but I can't give an ETA at the moment. For the time being, you can confidently remove the last AuthorizationCodeWebApp.StateRandomLength characters from the state you received alongside the authorization code.

Let me know if you have more questions.

amanda-tarafa commented 1 month ago

@akordowski I'll be closing this issue as I believe I've replied to all your questions and things are now working. Leave a new comment is you believe otherwise.

akordowski commented 1 month ago

@amanda-tarafa Thank you for the explanation, that helped me a lot.

The random number added will always have AuthorizationCodeWebApp.StateRandomLength

I was confused by the name of the property and thought that the lenght of the state is random. As it is 8 chars in length, so I can just cut it from the end.

There's a TODO in the code to allow deactivating the addition of this random number

Any interest for a PR regarding this TODO?

Thank you for your help!

amanda-tarafa commented 1 month ago

Any interest for a PR regarding this TODO?

If you are up for a potentially slow review and back and forth, thent that's fine. We are somewhat busy at the moment. Also, consider that we wouldn't accept a breaking change for this, nor "weirdness" around flow creation, etc. Maybe draft a PR in terms of how the change will affect the library public surface, and when we have that pinned down you can work on implementation and tests.

akordowski commented 1 month ago

Ok, will see if I can spare some time ;) Thank you.