diff --git a/minecraft/auth/live.go b/minecraft/auth/live.go index 5e176739..30b21ec2 100644 --- a/minecraft/auth/live.go +++ b/minecraft/auth/live.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "os" + "sync" "time" "golang.org/x/oauth2" @@ -78,7 +79,8 @@ func RequestLiveTokenWriter(w io.Writer) (*oauth2.Token, error) { if err != nil { return nil, err } - _, _ = w.Write([]byte(fmt.Sprintf("Authenticate at %v using the code %v.\n", d.VerificationURI, d.UserCode))) + + _, _ = fmt.Fprintf(w, "Authenticate at %v using the code %v.\n", d.VerificationURI, d.UserCode) ticker := time.NewTicker(time.Second * time.Duration(d.Interval)) defer ticker.Stop() @@ -97,6 +99,28 @@ func RequestLiveTokenWriter(w io.Writer) (*oauth2.Token, error) { panic("unreachable") } +var ( + serverTimeMu sync.Mutex + // serverTime represents the most recent server date received from Microsoft servers. + // It's used for the signed requests which can be blocked if the users device time is not synced. + // It uses the date received from the unsigned requests. + serverTime time.Time +) + +func updateServerTimeFromHeaders(headers http.Header) { + date := headers.Get("Date") + if date == "" { + return + } + t, err := time.Parse(time.RFC1123, date) + if err != nil || t.IsZero() { + return + } + serverTimeMu.Lock() + serverTime = t + serverTimeMu.Unlock() +} + // startDeviceAuth starts the device auth, retrieving a login URI for the user and a code the user needs to // enter. func startDeviceAuth() (*deviceAuthConnect, error) { @@ -128,13 +152,17 @@ func pollDeviceAuth(deviceCode string) (t *oauth2.Token, err error) { return nil, fmt.Errorf("POST https://login.live.com/oauth20_token.srf: %w", err) } defer resp.Body.Close() + + updateServerTimeFromHeaders(resp.Header) + poll := new(deviceAuthPoll) if err := json.NewDecoder(resp.Body).Decode(poll); err != nil { return nil, fmt.Errorf("POST https://login.live.com/oauth20_token.srf: json decode: %w", err) } - if poll.Error == "authorization_pending" { + switch poll.Error { + case "authorization_pending": return nil, nil - } else if poll.Error == "" { + case "": return &oauth2.Token{ AccessToken: poll.AccessToken, TokenType: poll.TokenType, @@ -160,6 +188,9 @@ func refreshToken(t *oauth2.Token) (*oauth2.Token, error) { return nil, fmt.Errorf("POST https://login.live.com/oauth20_token.srf: %w", err) } defer resp.Body.Close() + + updateServerTimeFromHeaders(resp.Header) + poll := new(deviceAuthPoll) if err := json.NewDecoder(resp.Body).Decode(poll); err != nil { return nil, fmt.Errorf("POST https://login.live.com/oauth20_token.srf: json decode: %w", err) diff --git a/minecraft/auth/xbox.go b/minecraft/auth/xbox.go index e46d2542..a303a716 100644 --- a/minecraft/auth/xbox.go +++ b/minecraft/auth/xbox.go @@ -72,7 +72,7 @@ func obtainXBLToken(ctx context.Context, c *http.Client, key *ecdsa.PrivateKey, data, err := json.Marshal(map[string]any{ "AccessToken": "t=" + liveToken.AccessToken, "AppId": "0000000048183522", - "deviceToken": device.Token, + "DeviceToken": device.Token, "Sandbox": "RETAIL", "UseModernGamertag": true, "SiteName": "user.auth.xboxlive.com", @@ -102,6 +102,9 @@ func obtainXBLToken(ctx context.Context, c *http.Client, key *ecdsa.PrivateKey, return nil, fmt.Errorf("POST %v: %w", "https://sisu.xboxlive.com/authorize", err) } defer resp.Body.Close() + + updateServerTimeFromHeaders(resp.Header) + if resp.StatusCode != 200 { // Xbox Live returns a custom error code in the x-err header. if errorCode := resp.Header.Get("x-err"); errorCode != "" { @@ -155,6 +158,9 @@ func obtainDeviceToken(ctx context.Context, c *http.Client, key *ecdsa.PrivateKe if err != nil { return nil, fmt.Errorf("POST %v: %w", "https://device.auth.xboxlive.com/device/authenticate", err) } + + updateServerTimeFromHeaders(resp.Header) + defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("POST %v: %v", "https://device.auth.xboxlive.com/device/authenticate", resp.Status) @@ -166,7 +172,16 @@ func obtainDeviceToken(ctx context.Context, c *http.Client, key *ecdsa.PrivateKe // sign signs the request passed containing the body passed. It signs the request using the ECDSA private key // passed. If the request has a 'ProofKey' field in the Properties field, that key must be passed here. func sign(request *http.Request, body []byte, key *ecdsa.PrivateKey) { - currentTime := windowsTimestamp() + serverTimeMu.Lock() + currentServerDate := serverTime + serverTimeMu.Unlock() + var currentTime int64 + if !currentServerDate.IsZero() { + currentTime = windowsTimestamp(currentServerDate) + } else { // Should never happen + currentTime = windowsTimestamp(time.Now()) + } + hash := sha256.New() // Signature policy version (0, 0, 0, 1) + 0 byte. @@ -214,8 +229,8 @@ func sign(request *http.Request, body []byte, key *ecdsa.PrivateKey) { // windowsTimestamp returns a Windows specific timestamp. It has a certain offset from Unix time which must be // accounted for. -func windowsTimestamp() int64 { - return (time.Now().Unix() + 11644473600) * 10000000 +func windowsTimestamp(t time.Time) int64 { + return (t.Unix() + 11644473600) * 10000000 } // padTo32Bytes converts a big.Int into a fixed 32-byte, zero-padded slice. diff --git a/minecraft/dial.go b/minecraft/dial.go index aa17bf01..04be5686 100644 --- a/minecraft/dial.go +++ b/minecraft/dial.go @@ -357,18 +357,19 @@ func getXBLToken(ctx context.Context, dialer Dialer) (*auth.XBLToken, error) { if err != nil { return nil, fmt.Errorf("request Live Connect token: %w", err) } + xblToken, err := auth.RequestXBLToken(ctx, liveToken, "https://multiplayer.minecraft.net/") if err != nil { return nil, fmt.Errorf("request XBOX Live token: %w", err) } + return xblToken, nil } // authChain requests the Minecraft auth JWT chain using the credentials passed. If successful, an encoded // chain ready to be put in a login request is returned. func authChain(ctx context.Context, xblToken *auth.XBLToken, key *ecdsa.PrivateKey) (string, error) { - - // Obtain the raw chain data using the + // Obtain the raw chain data using the XBL token. chain, err := auth.RequestMinecraftChain(ctx, xblToken, key) if err != nil { return "", fmt.Errorf("request Minecraft auth chain: %w", err)