173 lines
4.1 KiB
Go
173 lines
4.1 KiB
Go
|
package graphql
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"net/http"
|
||
|
"time"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
"github.com/xlzd/gotp"
|
||
|
|
||
|
"git.lehouerou.net/laurent/sorare/bcrypt"
|
||
|
)
|
||
|
|
||
|
const bCryptCost = 11
|
||
|
|
||
|
func (c *Client) getSaltFromEmail(ctx context.Context, email string) (string, error) {
|
||
|
var response struct {
|
||
|
Salt string `json:"salt"`
|
||
|
}
|
||
|
req, err := http.NewRequestWithContext(
|
||
|
ctx,
|
||
|
http.MethodGet,
|
||
|
"https://api.sorare.com/api/v1/users/"+email,
|
||
|
nil,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return "", errors.Wrap(err, "creating request")
|
||
|
}
|
||
|
resp, err := c.httpClient.Do(req)
|
||
|
if err != nil {
|
||
|
return "", errors.Wrap(err, "executing request")
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
return "", errors.Errorf("executing request failed with status code %d", resp.StatusCode)
|
||
|
}
|
||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||
|
if err != nil {
|
||
|
return "", errors.Wrap(err, "decoding response")
|
||
|
}
|
||
|
return response.Salt[7:], nil
|
||
|
}
|
||
|
|
||
|
func hashPasswordWithSalt(password, salt string) (string, error) {
|
||
|
hashed, err := bcrypt.GenerateFromPasswordAndSalt([]byte(password), []byte(salt), bCryptCost)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
return string(hashed), nil
|
||
|
}
|
||
|
|
||
|
type JwtToken struct {
|
||
|
Token string
|
||
|
ExpiredAt time.Time
|
||
|
}
|
||
|
|
||
|
type SignIn struct {
|
||
|
CurrentUser struct {
|
||
|
Slug string
|
||
|
JwtToken JwtToken `graphql:"jwtToken(aud: $aud)"`
|
||
|
} `graphql:"currentUser"`
|
||
|
OtpSessionChallenge string
|
||
|
Errors []struct {
|
||
|
Message string
|
||
|
} `graphql:"errors"`
|
||
|
}
|
||
|
|
||
|
type signInInput struct {
|
||
|
Email string `json:"email,omitempty"`
|
||
|
Password string `json:"password,omitempty"`
|
||
|
OtpSessionChallenge string `json:"otpSessionChallenge,omitempty"`
|
||
|
OtpAttempt string `json:"otpAttempt,omitempty"`
|
||
|
}
|
||
|
|
||
|
type SignInParams struct {
|
||
|
Input signInInput `graphql:"input"`
|
||
|
}
|
||
|
|
||
|
type SignInMutation struct {
|
||
|
*Mutation[SignIn, SignInParams]
|
||
|
}
|
||
|
|
||
|
func NewSignInMutation(c *Client, audience string) *SignInMutation {
|
||
|
return &SignInMutation{
|
||
|
NewMutation[SignIn, SignInParams](c, "signIn").WithQueryParam("aud", audience),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Client) getNewToken(
|
||
|
ctx context.Context,
|
||
|
email string,
|
||
|
password string,
|
||
|
audience string,
|
||
|
otp string,
|
||
|
) (JwtToken, error) {
|
||
|
|
||
|
salt, err := c.getSaltFromEmail(ctx, email)
|
||
|
if err != nil {
|
||
|
return JwtToken{}, errors.Wrap(err, "getting salt from email")
|
||
|
}
|
||
|
hashed, err := hashPasswordWithSalt(password, salt)
|
||
|
if err != nil {
|
||
|
return JwtToken{}, errors.Wrap(err, "hashing password with salt")
|
||
|
}
|
||
|
|
||
|
signInMutation := NewSignInMutation(c, audience)
|
||
|
res, err := signInMutation.Execute(ctx, SignInParams{
|
||
|
Input: signInInput{
|
||
|
Email: email,
|
||
|
Password: hashed,
|
||
|
},
|
||
|
})
|
||
|
if err != nil {
|
||
|
return JwtToken{}, errors.Wrap(err, "executing signin mutation with email and password")
|
||
|
}
|
||
|
|
||
|
if len(res.Errors) == 0 {
|
||
|
if res.CurrentUser.JwtToken.Token == "" {
|
||
|
return JwtToken{}, errors.New(
|
||
|
"no errors & no jwt token returned on email/password mutation",
|
||
|
)
|
||
|
}
|
||
|
return res.CurrentUser.JwtToken, nil
|
||
|
}
|
||
|
|
||
|
if res.Errors[0].Message == "invalid" {
|
||
|
return JwtToken{}, errors.New("invalid email or password")
|
||
|
}
|
||
|
|
||
|
if res.Errors[0].Message == "2fa_missing" {
|
||
|
if otp == "" {
|
||
|
return JwtToken{}, errors.New("2fa missing & no otp provided")
|
||
|
}
|
||
|
token := gotp.NewDefaultTOTP(otp).Now()
|
||
|
|
||
|
signInMutation := NewSignInMutation(c, audience)
|
||
|
resOtp, err := signInMutation.Execute(ctx, SignInParams{
|
||
|
Input: signInInput{
|
||
|
OtpSessionChallenge: res.OtpSessionChallenge,
|
||
|
OtpAttempt: token,
|
||
|
},
|
||
|
})
|
||
|
if err != nil {
|
||
|
return JwtToken{}, err
|
||
|
}
|
||
|
|
||
|
if len(resOtp.Errors) == 0 {
|
||
|
if resOtp.CurrentUser.JwtToken.Token == "" {
|
||
|
return JwtToken{}, errors.New("no errors & no jwt token returned on otp mutation")
|
||
|
}
|
||
|
return resOtp.CurrentUser.JwtToken, nil
|
||
|
}
|
||
|
return JwtToken{}, errors.Errorf("validating otp: %s", resOtp.Errors[0].Message)
|
||
|
}
|
||
|
return JwtToken{}, errors.Errorf("unmanaged error: %s", res.Errors[0].Message)
|
||
|
}
|
||
|
|
||
|
func (c *Client) Authenticate(
|
||
|
ctx context.Context,
|
||
|
email string,
|
||
|
password string,
|
||
|
audience string,
|
||
|
otp string,
|
||
|
) error {
|
||
|
token, err := c.getNewToken(ctx, email, password, audience, otp)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "getting new jwt token")
|
||
|
}
|
||
|
c.SetJWTToken(token, audience)
|
||
|
return nil
|
||
|
}
|