mattn / go-mastodon

mastodon client for golang
MIT License
604 stars 88 forks source link

Re-using access token #194

Closed coolapso closed 1 month ago

coolapso commented 1 month ago

Apologies if this has been answered here, but I am having some trouble with authentication, wonder what I could be missing and if you can point me in the right direction

I'm writing a CLI application, and it contains a "configure" action myapplication configure that:

Then an Action to create a post myapplication -m "text"

Now, the problem with this is:

I am able to create a post After generating a configuration file, the next time I try to make a post re-using the exact same accessToken, I will get Invalid_grant If I move the Authenticate access token to the configuration step (before saving everything into the configuration file), When I try to create a post I get The access token is invalid.

☸ virt01 ❯go run main.go configure
Mastodon Server (https://mastodon.social):
Open your browser to
https://mastodon.social/oauth/authorize?client_id=xxxxxxxx&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&scope=read+write+follow
 and copy/paste the given token
Paste the token here:xxxxxxxxxx

[coolapso@nebu]-[~/megophone]  dev go v1.23.2  15s
☸ virt01 ❯go run main.go -m "test"
Posting...
Toot created with ID: 113301936934081496
Done! 

[coolapso@nebu]-[~/megophone]  dev go v1.23.2
☸ virt01 ❯go run main.go -m "test"
Posting...
Mastodon Authentication failed, bad authorization: 400 Bad Request: invalid_grant

exit status 1

Do I need to request users to copy pate the link get a new token and paste and Authenticate the token every time I want to make the post?

I took a look at this issue and it's mentioned in this comment that AuthenticateToken() only needs to be used once, But it seems the token becomes invalid as soon as it is used once, and its not accepted if its authenticated and then re-used on another "session" of the same application.

Genuinely confused. Thanks a lot for your time and help!

coolapso commented 1 month ago

Giving also some bit more info, in the "semi working" scenario ....

This works:

func mastodonClientConfig() *gomasto.Config {
    return &gomasto.Config{
        Server:       viper.GetString("mastodon_server"),
        ClientID:     viper.GetString("mastodon_client_id"),
        ClientSecret: viper.GetString("mastodon_client_secret"),
        AccessToken:  viper.GetString("mastodon_access_token"),
    }
}

func postMastodon(text, mediaPath string) (err error) {

    config := mastodonClientConfig()
    client := gomasto.NewClient(config)

    if mediaPath != "" {
        return 
    }

    if err := client.AuthenticateToken(context.Background(), config.AccessToken, redirectUri); err != nil {
        return fmt.Errorf("Mastodon Authentication failed, %v\n", err)
    }

    id, err := mastodon.CreatePost(context.Background(), client, text, "public")
    if err != nil { 
        return fmt.Errorf("Failed to post to mastodon, %v\n", err)
    }

    fmt.Println("Toot created with ID:", id)
    return nil
}

This not doesn't (shouldn't it be the same thing??):

func mastodonClientConfig() *gomasto.Config {
    return &gomasto.Config{
        Server:       viper.GetString("mastodon_server"),
        ClientID:     viper.GetString("mastodon_client_id"),
        ClientSecret: viper.GetString("mastodon_client_secret"),
        AccessToken:  viper.GetString("mastodon_access_token"),
    }
}

func authenticateToken(ctx context.Context, accessToken string) error {
    client := gomasto.NewClient(mastodonClientConfig())
    return client.AuthenticateToken(ctx, accessToken, redirectUri)
}

func postMastodon(text, mediaPath string) (err error) {

    config := mastodonClientConfig()
    client := gomasto.NewClient(config)

    fmt.Println(client.Config)
    fmt.Println(client.UserAgent)

    if mediaPath != "" {
        return 
    }

    if err := authenticateToken(context.Background(), c.m.GetAccessToken()); err != nil {
        return fmt.Errorf("Failed to authenticate access token, %v\n", err)
    }

    id, err := mastodon.CreatePost(context.Background(), client, text, "public")
    if err != nil { 
        return fmt.Errorf("Failed to post to mastodon, %v\n", err)
    }

    fmt.Println("Toot created with ID:", id)
    return nil
}

It looks like the requests have to always be made by the same client that authenticated, otherwise it doesn't work, even tho the clients look exactly the same and use exactly the same configurations :thinking: so confused.

coolapso commented 1 month ago

Hey, here's what I think the issue is ... unless I am understanding this wrong, I believe this is actually a bug in the library and a unfortunate naming issue.

TL;DR

the c.authenticate() method, is broken. It should return the access token in order to save it, but instead sets it in the current instance of the client.

AuthenticateToken is an unfortunate naming, because when calling c.authenticate what it is actually doing is to exchange an authorization code for an AccessToken

The Long road

When using oauth authentication with mastodon the flow is as follow:

Looking deeper into the code we can see that AuthenticateToken() returns c.authenticate() and that c.authenticate actually makes the request to the mastodon oauth/token endpoint, which according to the mastodon documentation here is used to obtain the Access Token and not to "authenticate" the token.

Here's the curl example:

curl -X POST \
        -F "client_id=${CLIENT_ID}" \
        -F "client_secret=${CLIENT_SECRET}" \
        -F 'redirect_uri=urn:ietf:wg:oauth:2.0:oob' \
        -F 'grant_type=authorization_code' \
        -F 'code=********************************' \
        -F 'scope=read write push' \
        https://mastodon.social/oauth/token
{"access_token":"*********************","token_type":"Bearer","scope":"read write push","created_at":1729194979}% 

this being said the c.authenticate() method should be returning both a error and the token string, so it could be saved and be re-used but instead it is setting the AccessToken only in the current instance of the client which explains why it works the first time and not the second time and why it works in one side of the code and not in the other.

a) The client is effectively different b) every time we call the AuthenticateToken we are actually trying to exchange the the access code by a token, however that access code will be already invalided because it was already used to exchange for a token.

The work around

Until a PR is accepted and merged, after using AuthenticateToken the value that should be saved is the access token in the current instance of the client. accessToken := client.Config.AccessToken

Uinelj commented 1 month ago

Got bit by this aswell.

The README.md code excerpt should show how to fetch (without necessarily stating how to store) the AccessToken.

Changing authenticate's return values would bubble up in several other function signatures and this would be somewhat breaking.

Uinelj commented 1 month ago

195 I added a comment here, but I'd be down to fix this in the code :)

coolapso commented 1 month ago

I am planning to put up a PR with a RFC with a fix for this during this week.

Changing the method is definitely not a great idea ... but my thoughts are around marking the method as deprecated,, to create the necessary methods to fix this issue and of course adjust all the documentation regarding this. This should ensure backwards compatibility and allow the improvement of the whole authentication flow.