discordgo icon indicating copy to clipboard operation
discordgo copied to clipboard

Support Client Credentials Grant

Open sntran opened this issue 3 years ago • 3 comments

Docs: https://discord.com/developers/docs/topics/oauth2#client-credentials-grant

Now that Slash Command is supported, which does not always require a Bot, please consider adding support to authenticate just the application itself using its client_id and client_secret.

Understandingly, discordgo.New already supports Bearer token, which will work for the application, but the user still needs to retrieve the token using the above Client Credentials flow.

Either revisit discordgo.New to add support for the new flow, or provide a user-usable function on the session to authenticate for a token.

sntran avatar Jul 06 '21 18:07 sntran

I have implemented a basic client_credentials request, however, the token returned seems to be invalid, or cannot be used by discodgo.New. The API returns TokenType = "Bearer", and discordgo errors out with websocket: close 4004: Authentication failed. I have tried overriding the TokenType to "Bot", no change.

type Credentials struct {
        Token     string `json:"access_token"`
        TokenType string `json:"token_type"` // Bearer
        ExpiresIn int    `json:"expires_in"`
        Scope     string `json:"scope"`

        Expires time.Time `json:"-"`
}

func NewCredentials(id, secret string, scopes ...string) (*Credentials, error) {
        wrap := func(err error) (*Credentials, error) {
                return nil, err
        }
        if len(scopes) == 0 {
                scopes = []string{"identify", "connections"}
        }

        endpoint := fmt.Sprintf("https://%s:%[email protected]/api/v8/oauth2/token", id, secret)

        params := url.Values{}
        params.Add("grant_type", "client_credentials")
        params.Add("scope", strings.Join(scopes, " "))

        // Create request
        req, err := http.NewRequest("POST", endpoint, strings.NewReader(params.Encode()))
        if err != nil {
                return wrap(err)
        }
        req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

        // Issue request
        res, err := http.DefaultClient.Do(req)
        if err != nil {
                return wrap(err)
        }
        defer res.Body.Close()

        // Read response
        body, err := ioutil.ReadAll(res.Body)
        if err != nil {
                return wrap(err)
        }

        // Decode response
        c := &Credentials{}
        if err := json.Unmarshal(body, c); err != nil {
                return wrap(err)
        }
        
        // Expire token before it expires on discord side
        c.Expires = time.Now().Add(time.Second * time.Duration(c.ExpiresIn*90/100))

        return c, nil
}

func (c *Credentials) String() string {
        return c.TokenType + " " + c.Token
}

It is possible that I'm doing something wrong, I'm adding the code here if somebody wants to test it or if they figure out the issue with it. I suppose it's unreasonable to suggest that discord haven't implemented client_credentials grant correctly, but I do note that each POST request returns an identical token value.

It's also possible I need different scopes (I have seen a bot scope in there...)

titpetric avatar Aug 23 '21 09:08 titpetric

The client credential flow is a quick and easy way for bot developers to get their own bearer tokens for testing purposes.

The returned token is also not a JWT (unlike a OAuth2 grant token which is generated on the apps page). I think I'm stepping away from debugging this for now, but feedback is welcome.

titpetric avatar Aug 23 '21 09:08 titpetric

It was explained to me, that the retrieved token from the code above can't be used to generate Bots credentials (hence the 4004 error on websocket), but it could be used to manage slash commands as mentioned in the OP issue above. A bit invalid for my purposes, but it should work for y'all if you're able to feed it into the APIs.

titpetric avatar Aug 24 '21 06:08 titpetric