glacasa / Mastonet

C# Library for Mastodon
MIT License
225 stars 37 forks source link

Mastonet.ServerErrorException: invalid_grant at Auth? auth = await authClient.ConnectWithCode(userAuth.Code); #100

Closed JeepNL closed 1 year ago

JeepNL commented 1 year ago

Info

(update)

Mastonet version: 2.2.1 .NET SDK: 7.0.200-preview.22628.1 Instance: mastodon.social Project: Front-end Blazor WASM with back-end Web API

Problem

I'm getting a Mastonet.ServerErrorException: invalid_grant at the Web API backend Auth? auth = await authClient.ConnectWithCode(userAuth.Code); when I try to get the AccessToken and I do not know what I'm doing wrong.

This is the last step in my WebAPI to receive the user's Access Token, right after the user has authorized Mastodon to use my app (see Image 1)

I saved the AppRegistration in a database, and I initialize AuthenticationClient with that info. Every field is filled with the (I think) correct value πŸ‘‡ (update)

***** authClient.AppRegistration.Id: 1859380
***** authClient.AppRegistration.Instance: mastodon.social
***** authClient.AppRegistration.Scope: Read, Write, Follow
***** authClient.AppRegistration.RedirectUri: https://localhost/auth/return
***** authClient.AppRegistration.ClientId: [REDACTED]
***** authClient.AppRegistration.ClientSecret: [REDACTED]

Part of Web API Controller

    [HttpPost]
    public async Task<ActionResult<string>> AccessToken(UserAuth userAuth) // Step 3
    {
        MasInstance? dbInstance = await _dbContext.MasInstances
            .Where(w => w.Name == userAuth.Instance.ToLower())
            .AsNoTracking()
            .SingleOrDefaultAsync();

        AppRegistration? appRegistration = new()
        {
            Id = dbInstance!.RegistrationId,
            Instance = dbInstance.Name,
            RedirectUri = dbInstance.RedirUrl,
            Scope = (Scope)dbInstance.Scope, // = 7 (int) = Scope.Read | Scope.Write | Scope.Follow
            ClientId = dbInstance.ClientId,
            ClientSecret = dbInstance.ClientSecret
        };

        AuthenticationClient authClient = new(appRegistration);

        Console.WriteLine($"\n***** userAuth.Code: {userAuth.Code}\n");

        // Error happens here πŸ‘‡
        Auth? auth = await authClient.ConnectWithCode(userAuth.Code); // Line 54 in my Web API controller

        Console.WriteLine($"\n***** auth.AccessToken: {auth.AccessToken}\n");
        return Ok(auth.AccessToken);
    }

UserAuth.cs

namespace SharedProject.Tusk.Models;
public class UserAuth
{
    public string Instance { get; set; } = string.Empty;
    public string Code { get; set; } = string.Empty;
}

Error Dev Console (Response)

Mastonet.ServerErrorException: invalid_grant
   at Mastonet.BaseHttpClient.TryDeserialize[T](String json)
   at Mastonet.BaseHttpClient.Post[T](String route, IEnumerable`1 data, IEnumerable`1 media)
   at Api.Tusk.Controllers.MastoAuthController.AccessToken(UserAuth userAuth) in C:\X\Repos\Tusk\ApiTusk\Controllers\MastoAuthController.cs:line 54
   at lambda_method35(Closure, Object)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: */*
Host: localhost:8081
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.0.0
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,nl;q=0.8
Cache-Control: no-cache
Content-Type: application/json; charset=utf-8
Origin: https://localhost
Pragma: no-cache
Referer: https://localhost/
Content-Length: 83
sec-ch-ua: "Microsoft Edge";v="111", "Not(A:Brand";v="8", "Chromium";v="111"
sec-ch-ua-platform: "Windows"
DNT: 1
sec-ch-ua-mobile: ?0
sec-fetch-site: same-site
sec-fetch-mode: cors
sec-fetch-dest: empty
sec-gpc: 1

Error Web API Console

info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[105]
      Executed action Api.Tusk.Controllers.MastoAuthController.AccessToken (Api.Tusk) in 116.3033ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'Api.Tusk.Controllers.MastoAuthController.AccessToken (Api.Tusk)'
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      Mastonet.ServerErrorException: invalid_grant
         at Mastonet.BaseHttpClient.TryDeserialize[T](String json)
         at Mastonet.BaseHttpClient.Post[T](String route, IEnumerable`1 data, IEnumerable`1 media)
         at Api.Tusk.Controllers.MastoAuthController.AccessToken(UserAuth userAuth) in C:\X\Repos\Tusk\ApiTusk\Controllers\MastoAuthController.cs:line 54
         at lambda_method35(Closure, Object)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/2 POST https://localhost:8081/Api/MastoAuth/AccessToken application/json;+charset=utf-8 83 - 500 - text/plain;+charset=utf-8 118.4494ms

Image 1

Screenshot 2023-02-05 133739

JeepNL commented 1 year ago

Could it be because there's no scope defined here (ConnectWithCode):

https://github.com/glacasa/Mastonet/blob/34d032e9ab2b773126520ad88b61d795585e03be/Mastonet/AuthenticationClient.cs#L85

JeepNL commented 1 year ago

[edit]

See the Mastodon API docs here (obtain the token, grant_type=authorization_code needs a scope?) :

https://docs.joinmastodon.org/client/authorized/#token

And here:

https://docs.joinmastodon.org/methods/oauth/#token

scope

String. List of requested OAuth scopes, separated by spaces (or by pluses, if using query parameters). If code was provided, then this must be equal to the scope requested from the user. Otherwise, it must be a subset of scopes declared during app registration. If not provided, defaults to read.

JeepNL commented 1 year ago

Well, that's (Scope) not it.

When I do this (without scope) in an Ubuntu environment

curl -X POST \
        -F 'client_id=[REDACTED] \
        -F 'client_secret=[REDACTED] \
        -F 'redirect_uri=https://localhost/auth/return' \
        -F 'grant_type=authorization_code' \
        -F 'code=[REDACTED]' \
        https://mastodon.social/oauth/token

I'm getting the AccessToken.

{"access_token":"[REDACTED]","token_type":"Bearer","scope":"read write follow","created_at":1675610689}

But (still) not when I call Auth? auth = await authClient.ConnectWithCode(userAuth.Code); in my Web API.

glacasa commented 1 year ago

Maybe what is missing is the redirect_uri

Did you register the app with a redirect_uri provided ? if so, maybe you need to use the same when connecting with code. You provided it on your curl test

When calling ConnectWithCode, the redirect_uri is an optional argument :

Auth? auth = await authClient.ConnectWithCode(userAuth.Code, "https://localhost/auth/return"); 
JeepNL commented 1 year ago

Sorry, just tried that, and it gives the same error.

I did register the app with that same (and now only 1) url. Maybe it's URL Encoding somewhere? I don't know, I'm just guessing.

glacasa commented 1 year ago

Not sure what is going on, everything seems ok

You can try to use WebUtility.UrlEncode πŸ€·β€β™€οΈ

glacasa commented 1 year ago

Did you follow the docs ? https://github.com/glacasa/Mastonet/blob/main/DOC.md

Maybe check the sample code to see if there are differences ? https://github.com/glacasa/Mastonet.SampleApp/blob/master/Mastonet.SampleApp/Login.xaml.cs

JeepNL commented 1 year ago

Yes, I've read the docs (DOC.md) and followed it. I'll need to read the sample maybe more carefully :)

I've created a temporary GitHub Repo with my project here (all of the code, work in progress)

πŸ‘‰ https://github.com/JeepNL/Tusk

JeepNL commented 1 year ago

That project should run out of the box locally (localhost) I think. De (SQLite) DB is provided, but empty. To test you should enter the Mastodon Instance you use to login.

Note: when the app redirects you in your browser to your instance for authentication, the first time maybe you'll see an error. If you use F5 (refresh browser) the remote auth page will show correctly. The URL (in the brower) is correct, I don't know yet why this is happening ...

JeepNL commented 1 year ago

'Hourra' πŸ˜‰

I've got the access token!

I've changed a couple of things, I need to check what exactly made it work but I think it was the CORS definition in Program.cs

I'm not 100% sure, I've to check it tomorrow because my head is dizzy ...

Program.cs

using Api.Tusk.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSystemd();

// Add services to the container.
string? connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString!));

string[] origins = builder.Configuration["Origins"]!.Split(";");
builder.Services.AddCors(options =>
{
    options.AddPolicy("ApiPolicy", builder =>
    {
        builder.WithOrigins(origins)
        .SetIsOriginAllowedToAllowWildcardSubdomains()
        .AllowCredentials()
        .AllowAnyMethod()
        .AllowAnyHeader()
        .WithExposedHeaders("*");
    });
});

builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(365);
});

builder.Services.AddControllers();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseCors("ApiPolicy");
app.MapControllers().RequireCors("ApiPolicy");
app.MapGet("/", () => "Error #1");
app.MapGet("/Error", () => "Error #2");

app.Run();
JeepNL commented 1 year ago

FYI:

I've encountered several problems with my code and with the remote Mastodon instance (mastodon.social) but it has little to do with your library Mastonet (except when you've a multiline RedirectUri, more about that later).

Many times my mastodon instance, mastodon.social, returned "invalid_grant", but if I retried it with the same code, the instance returned the AccessToken

So, now I've implemented a 'retry', with 3.33 seconds delay, in my code, and it works somewhat better.

Web API Console

***** authClient.AppRegistration.Id: 1863056
***** authClient.AppRegistration.Instance: mastodon.social
***** authClient.AppRegistration.Scope: Read, Write, Follow
***** authClient.AppRegistration.RedirectUri: https://localhost/auth/return
***** authClient.AppRegistration.ClientId: [REDACTED]
***** authClient.AppRegistration.ClientSecret: [REDACTED]

***** (AccessToken) TRY: 1

***** (AccessToken) ERROR: 1 - RETRY
***** Exception Mastonet.ServerErrorException: invalid_grant
   at Mastonet.BaseHttpClient.TryDeserialize[T](String json)
   at Mastonet.BaseHttpClient.Post[T](String route, IEnumerable`1 data, IEnumerable`1 media)
   at Api.Tusk.Controllers.MastoAuthController.AccessToken(UserAuth userAuth) in C:\X\Repos\Tusk\ApiTusk\Controllers\MastoAuthController.cs:line 61

***** (AccessToken) TRY: 2

***** (AccessToken) ERROR: 2 - RETRY
***** Exception Mastonet.ServerErrorException: invalid_grant
   at Mastonet.BaseHttpClient.TryDeserialize[T](String json)
   at Mastonet.BaseHttpClient.Post[T](String route, IEnumerable`1 data, IEnumerable`1 media)
   at Api.Tusk.Controllers.MastoAuthController.AccessToken(UserAuth userAuth) in C:\X\Repos\Tusk\ApiTusk\Controllers\MastoAuthController.cs:line 61

***** (AccessToken) TRY: 3

info: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
      Executing OkObjectResult, writing value of type 'System.String'.
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[105]
      Executed action Api.Tusk.Controllers.MastoAuthController.AccessToken (Api.Tusk) in 6903.3197ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'Api.Tusk.Controllers.MastoAuthController.AccessToken (Api.Tusk)'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/2 POST https://localhost:8081/Api/MastoAuth/AccessToken application/json;+charset=utf-8 83 - 200 - text/plain;+charset=utf-8 6903.6726ms