-
Notifications
You must be signed in to change notification settings - Fork 822
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
Add Login + 2FA #725
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this new |
||||||
// 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 | ||||||
|
@@ -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(), | ||||||
} | ||||||
|
@@ -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 { | ||||||
|
@@ -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 == "": | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||
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") | ||||||
|
@@ -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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This statement could be rewritten as:
Suggested change
|
||||||
} | ||||||
|
||||||
if s.Token == "" { | ||||||
return nil, ErrFailedToken | ||||||
} | ||||||
} | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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 { | ||||||||||||||||
|
@@ -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. | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Nit] Typo in function comment.
Suggested change
|
||||||||||||||||
func (s *Session) LoginMFA(email, password, mfa string) (err error) { | ||||||||||||||||
var login LoginResponse | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||
|
||||||||||||||||
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} | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Nit] This could be rewritten with field names for clarity:
Suggested change
|
||||||||||||||||
|
||||||||||||||||
resp, err := s.RequestWithBucketID( | ||||||||||||||||
"POST", EndpointTOTP, totp, EndpointTOTP, | ||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
if err != nil { | ||||||||||||||||
return nil, err | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
var login LoginResponse | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||
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. | ||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -119,6 +120,20 @@ type Session struct { | |
wsMutex sync.Mutex | ||
} | ||
|
||
type TOTP struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"` | ||
|
There was a problem hiding this comment.
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.