Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Login + 2FA #725

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 56 additions & 15 deletions discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,43 @@ import (
// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
const VERSION = "0.21.0-develop"

// ErrMFA will be risen by New when the user has 2FA.
// DefaultUserAgent is used when calling New().
var DefaultUserAgent = "DiscordBot (https://github.com/bwmarrin/discordgo, v" + VERSION + ")"

// ErrMFA will be returned by New when the user has 2FA but is not given the
// token.
var ErrMFA = errors.New("account has 2FA enabled")

// ErrFailedToken is returned if the Token is empty after authenticating.
var ErrFailedToken = errors.New("Unable to fetch discord authentication token")

type Login struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exported structs should have leading comment for godoc documentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this new Login struct and that it takes care of a TODO 👍

// Use either these 2
Email string `json:"email"`
Password string `json:"password"`

// Optionally use this with these 2 above
MFA string `json:"-"`

// This can be used instead
Token string `json:"-"`
}

// New creates a new Discord session and will automate some startup
// tasks if given enough information to do so. Currently you can pass zero
// arguments and it will return an empty Discord session.
// There are 3 ways to call New:
// There are 4 ways to call New:
// With a single auth token - All requests will use the token blindly,
// no verification of the token will be done and requests may fail.
// IF THE TOKEN IS FOR A BOT, IT MUST BE PREFIXED WITH `BOT `
// eg: `"Bot <token>"`
// With an email and password - Discord will sign in with the provided
// credentials.
// With an email, password and auth token - Discord will verify the auth
// With an email, password, and auth token - Discord will verify the auth
// token, if it is invalid it will sign in with the provided
// credentials. This is the Discord recommended way to sign in.
// With Login - refer to the comments in the struct. The behavior is similar
// to giving New an email, password, and auth token.
//
// NOTE: While email/pass authentication is supported by DiscordGo it is
// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token
Expand All @@ -58,7 +79,7 @@ func New(args ...interface{}) (s *Session, err error) {
ShardCount: 1,
MaxRestRetries: 3,
Client: &http.Client{Timeout: (20 * time.Second)},
UserAgent: "DiscordBot (https://github.com/bwmarrin/discordgo, v" + VERSION + ")",
UserAgent: DefaultUserAgent,
sequence: new(int64),
LastHeartbeatAck: time.Now().UTC(),
}
Expand All @@ -69,7 +90,7 @@ func New(args ...interface{}) (s *Session, err error) {
}

// Variables used below when parsing func arguments
var auth, pass string
var auth, pass, mfa string

// Parse passed arguments
for _, arg := range args {
Expand Down Expand Up @@ -113,8 +134,30 @@ func New(args ...interface{}) (s *Session, err error) {
return
}

// case Config:
// TODO: Parse configuration struct
case Login, *Login:
var l Login

switch v := v.(type) {
case Login:
l = v
case *Login:
l = *v
}

switch {
case l.Token != "":
auth = l.Token

case l.Email == "" && l.Password == "":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this comparison be a logical OR instead of AND? E.g., what happens if l.Email != nil, but l.Password == nil?

Suggested change
case l.Email == "" && l.Password == "":
case l.Email == "" || l.Password == "":

err = errors.New(
"missing either token or username and password")
return

default:
auth = l.Email
pass = l.Password
mfa = l.MFA
}

default:
err = fmt.Errorf("unsupported parameter type provided")
Expand All @@ -129,14 +172,12 @@ func New(args ...interface{}) (s *Session, err error) {
if pass == "" {
s.Token = auth
} else {
err = s.Login(auth, pass)
if err != nil || s.Token == "" {
if s.MFA {
err = ErrMFA
} else {
err = fmt.Errorf("Unable to fetch discord authentication token. %v", err)
}
return
if err = s.LoginMFA(auth, pass, mfa); err != nil {
return nil, fmt.Errorf(ErrFailedToken.Error()+". %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This statement could be rewritten as:

Suggested change
return nil, fmt.Errorf(ErrFailedToken.Error()+". %v", err)
return nil, fmt.Errorf("%s: %s", ErrFailedToken, err)

}

if s.Token == "" {
return nil, ErrFailedToken
}
}

Expand Down
1 change: 1 addition & 0 deletions endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var (
EndpointCDNBanners = EndpointCDN + "banners/"

EndpointAuth = EndpointAPI + "auth/"
EndpointTOTP = EndpointAuth + "mfa/totp"
EndpointLogin = EndpointAuth + "login"
EndpointLogout = EndpointAuth + "logout"
EndpointVerify = EndpointAuth + "verify"
Expand Down
79 changes: 74 additions & 5 deletions restapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,10 @@ func unmarshal(data []byte, v interface{}) error {
// Also, doing any form of automation with a user (non Bot) account may result
// in that account being permanently banned from Discord.
func (s *Session) Login(email, password string) (err error) {

data := struct {
Email string `json:"email"`
Password string `json:"password"`
}{email, password}
data := Login{
Email: email,
Password: password,
}

response, err := s.RequestWithBucketID("POST", EndpointLogin, data, EndpointLogin)
if err != nil {
Expand All @@ -222,6 +221,76 @@ func (s *Session) Login(email, password string) (err error) {
return
}

// LoginMFA tries to log in with or without a 2FA code. Only email and password
// and required.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit] Typo in function comment.

Suggested change
// and required.
// are required.

func (s *Session) LoginMFA(email, password, mfa string) (err error) {
var login LoginResponse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit] This variable could be renamed for clarity to avoid confusion when reading it later in the function with the new Login struct. E.g:

Suggested change
var login LoginResponse
var loginResponse LoginResponse


if s.mfaTicket == "" {
data := Login{
Email: email,
Password: password,
}

resp, err := s.RequestWithBucketID(
"POST", EndpointLogin, data, EndpointLogin,
)

if err != nil {
return err
}

if err := unmarshal(resp, &login); err != nil {
return err
}
} else {
// Trigger the below TOTP call
login.MFA = true
}

if login.MFA {
if login.Ticket != "" {
s.mfaTicket = login.Ticket
}

if mfa != "" {
// An MFA ticket already exists, redundant to retry logging in
l, err := s.TOTP(s.mfaTicket, mfa)
if err != nil {
return err
}

login = *l
} else {
return ErrMFA
}
}

s.Token = login.Token
s.MFA = login.MFA
return
}

// TOTP finishes logging in by sending over the final 2FA code.
func (s *Session) TOTP(ticket, code string) (*LoginResponse, error) {
totp := TOTP{code, nil, nil, ticket}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit] This could be rewritten with field names for clarity:

Suggested change
totp := TOTP{code, nil, nil, ticket}
totp := TOTP{
Code: code,
GiftCodeSkuID: nil,
LoginSource: nil,
Ticket: ticket,
}


resp, err := s.RequestWithBucketID(
"POST", EndpointTOTP, totp, EndpointTOTP,
)

if err != nil {
return nil, err
}

var login LoginResponse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit] This variable could be renamed for clarity to avoid confusion when reading it later in the functiion with the new Login struct. E.g:

Suggested change
var login LoginResponse
var loginResponse LoginResponse

if err = unmarshal(resp, &login); err != nil {
return nil, err
}

return &login, nil
}

// Register sends a Register request to Discord, and returns the authentication token
// Note that this account is temporary and should be verified for future use.
// Another option is to save the authentication token external, but this isn't recommended.
Expand Down
19 changes: 17 additions & 2 deletions structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ type Session struct {
// General configurable settings.

// Authentication token for this session
Token string
MFA bool
Token string
MFA bool
mfaTicket string

// Debug for printing JSON request/responses
Debug bool // Deprecated, will be removed.
Expand Down Expand Up @@ -119,6 +120,20 @@ type Session struct {
wsMutex sync.Mutex
}

type TOTP struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exported structs should have leading comment for godoc documentation.

Code string `json:"code"`
GiftCodeSkuID *struct{} `json:"gift_code_sku_id"` // dunno
LoginSource *struct{} `json:"login_source"` // dunno
Ticket string `json:"ticket"`
}

type LoginResponse struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exported structs should have leading comment for godoc documentation.

MFA bool `json:"mfa"`
SMS bool `json:"sms"`
Ticket string `json:"ticket"`
Token string `json:"token"`
}

// UserConnection is a Connection returned from the UserConnections endpoint
type UserConnection struct {
ID string `json:"id"`
Expand Down
5 changes: 3 additions & 2 deletions wsapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -765,8 +765,9 @@ type identifyOp struct {
// identify sends the identify packet to the gateway
func (s *Session) identify() error {

properties := identifyProperties{runtime.GOOS,
"Discordgo v" + VERSION,
properties := identifyProperties{
runtime.GOOS,
DefaultUserAgent,
"",
"",
"",
Expand Down