├── .gitignore ├── LICENSE ├── README.md ├── account.go ├── account_test.go ├── go.mod ├── go.sum ├── msa.go ├── msa_ouath_device_flow.go ├── msa_test.go ├── types.go ├── upload.sh └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 kqzz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MC GO 2 | > ⚠ Not up to date, check MCsniperGO repository for latest implementation of Mojang API 3 | 4 | MC API requests implemented in go 5 | 6 | cli link: https://github.com/Kqzz/MCsniperGO 7 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | package mcgo 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | func (account *MCaccount) AuthenticatedReq(method string, url string, body io.Reader) (*http.Request, error) { 18 | req, err := http.NewRequest(method, url, body) 19 | if err != nil { 20 | return nil, err 21 | } 22 | if account.Bearer == "" { 23 | return nil, errors.New("account is not authenticated") 24 | } 25 | req.Header.Add("Authorization", "Bearer "+account.Bearer) 26 | req.Header.Set("Content-Type", "application/json") 27 | 28 | return req, nil 29 | } 30 | 31 | func (account *MCaccount) authenticate() error { 32 | payload := fmt.Sprintf(`{ 33 | "agent": { 34 | "name": "Minecraft", 35 | "version": 1 36 | }, 37 | "username": "%s", 38 | "password": "%s", 39 | "requestUser": true 40 | }`, account.Email, account.Password) 41 | 42 | u := bytes.NewReader([]byte(payload)) 43 | request, err := http.NewRequest("POST", "https://authserver.mojang.com/authenticate", u) 44 | request.Header.Set("Content-Type", "application/json") 45 | 46 | if err != nil { 47 | return err 48 | } 49 | 50 | resp, err := http.DefaultClient.Do(request) 51 | 52 | if err != nil { 53 | return err 54 | } 55 | 56 | defer resp.Body.Close() 57 | 58 | if resp.StatusCode < 300 { 59 | var AccountInfo authenticateReqResp 60 | b, err := ioutil.ReadAll(resp.Body) 61 | if err != nil { 62 | return err 63 | } 64 | err = json.Unmarshal(b, &AccountInfo) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | account.Bearer = AccountInfo.Accesstoken 70 | account.Username = AccountInfo.User.Username 71 | account.UUID = AccountInfo.User.ID 72 | return nil 73 | 74 | } else if resp.StatusCode == 403 { 75 | return errors.New("invalid email or password") 76 | } 77 | return errors.New("reached end of authenticate function! Shouldn't be possible. most likely 'failed to auth' status code changed") 78 | } 79 | 80 | func (account *MCaccount) loadSecurityQuestions() error { 81 | req, err := account.AuthenticatedReq("GET", "https://api.mojang.com/user/security/challenges", nil) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | resp, err := http.DefaultClient.Do(req) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if resp.StatusCode >= 400 { 92 | return fmt.Errorf("got status %v when requesting security questions", resp.Status) 93 | } 94 | 95 | defer resp.Body.Close() 96 | 97 | var sqAnswers []SqAnswer 98 | 99 | respBytes, err := ioutil.ReadAll(resp.Body) 100 | 101 | if err != nil { 102 | return err 103 | } 104 | 105 | err = json.Unmarshal(respBytes, &sqAnswers) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | account.SecurityQuestions = sqAnswers 111 | 112 | return nil 113 | } 114 | 115 | // load account information (username, uuid) into accounts attributes, if not already there. When using Mojang authentication it is not necessary to load this info, as it will be automatically loaded. 116 | func (account *MCaccount) LoadAccountInfo() error { 117 | req, err := account.AuthenticatedReq("GET", "https://api.minecraftservices.com/minecraft/profile", nil) 118 | if err != nil { 119 | return err 120 | } 121 | resp, err := http.DefaultClient.Do(req) 122 | 123 | if err != nil { 124 | return err 125 | } 126 | 127 | defer resp.Body.Close() 128 | 129 | if resp.StatusCode == 404 { 130 | return &RequestError{ 131 | StatusCode: resp.StatusCode, 132 | Err: errors.New("account does not own minecraft"), 133 | } 134 | } 135 | 136 | respBytes, err := ioutil.ReadAll(resp.Body) 137 | 138 | if err != nil { 139 | return err 140 | } 141 | 142 | var respJson accInfoResponse 143 | 144 | json.Unmarshal(respBytes, &respJson) 145 | 146 | account.Username = respJson.Name 147 | account.UUID = respJson.ID 148 | 149 | return nil 150 | } 151 | 152 | func (account *MCaccount) needToAnswer() (bool, error) { 153 | req, err := account.AuthenticatedReq("GET", "https://api.mojang.com/user/security/location", nil) 154 | if err != nil { 155 | return false, err 156 | } 157 | 158 | resp, err := http.DefaultClient.Do(req) 159 | 160 | if err != nil { 161 | return true, err 162 | } 163 | defer resp.Body.Close() 164 | 165 | if resp.StatusCode == 204 { 166 | return false, nil 167 | } 168 | if resp.StatusCode == 403 { 169 | return true, nil 170 | } 171 | return true, fmt.Errorf("status of %v in needToAnswer not expected", resp.Status) 172 | } 173 | 174 | func (account *MCaccount) submitAnswers() error { 175 | if len(account.SecurityAnswers) != 3 { 176 | return errors.New("not enough security question answers provided") 177 | } 178 | if len(account.SecurityQuestions) != 3 { 179 | return errors.New("security questions not properly loaded") 180 | } 181 | var jsonContent []submitPostJson 182 | for i, sq := range account.SecurityQuestions { 183 | jsonContent = append(jsonContent, submitPostJson{ID: sq.Answer.ID, Answer: account.SecurityAnswers[i]}) 184 | } 185 | jsonStr, err := json.Marshal(jsonContent) 186 | if err != nil { 187 | return err 188 | } 189 | req, err := account.AuthenticatedReq("POST", "https://api.mojang.com/user/security/location", bytes.NewBuffer([]byte(jsonStr))) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | resp, err := http.DefaultClient.Do(req) 195 | 196 | if err != nil { 197 | return err 198 | } 199 | 200 | if resp.StatusCode == 204 { 201 | return nil 202 | } 203 | 204 | defer resp.Body.Close() 205 | 206 | if resp.StatusCode == 403 { 207 | return errors.New("at least one security question answer was incorrect") 208 | } 209 | return fmt.Errorf("got status %v on post request for sqs", resp.Status) 210 | } 211 | 212 | // Runs all steps necessary to have a fully authenticated mojang account. It will submit email & pass and securitty questions (if necessary). 213 | func (account *MCaccount) MojangAuthenticate() error { 214 | err := account.authenticate() 215 | if err != nil { 216 | return err 217 | } 218 | 219 | account.loadSecurityQuestions() 220 | 221 | if len(account.SecurityQuestions) == 0 { 222 | account.Authenticated = true 223 | return nil 224 | } 225 | 226 | answerNeeded, err := account.needToAnswer() 227 | if err != nil { 228 | return err 229 | } 230 | 231 | if !answerNeeded { 232 | account.Authenticated = true 233 | return nil 234 | } 235 | 236 | err = account.submitAnswers() 237 | if err != nil { 238 | return err 239 | } 240 | 241 | account.Authenticated = true 242 | return nil 243 | } 244 | 245 | func (account *MCaccount) HasGcApplied() (bool, error) { 246 | bodyStr := `{"profileName": "test"}` 247 | req, err := account.AuthenticatedReq("POST", "https://api.minecraftservices.com/minecraft/profile", bytes.NewReader([]byte(bodyStr))) 248 | if err != nil { 249 | return false, err 250 | } 251 | 252 | resp, err := http.DefaultClient.Do(req) 253 | if err != nil { 254 | return false, err 255 | } 256 | 257 | defer resp.Body.Close() 258 | 259 | bodyBytes, err := ioutil.ReadAll(resp.Body) 260 | if err != nil { 261 | return false, err 262 | } 263 | 264 | if resp.StatusCode == 200 { 265 | return false, &RequestError{ 266 | StatusCode: resp.StatusCode, 267 | Err: errors.New("successfully created profile with name test.. unintended behavior, function is meant to check if gc is applied"), 268 | } 269 | } else if resp.StatusCode == 401 { 270 | return false, &RequestError{ 271 | StatusCode: resp.StatusCode, 272 | Err: errors.New("received unauthorized response"), 273 | } 274 | } else if resp.StatusCode == 400 { 275 | var respError hasGcAppliedResp 276 | 277 | err = json.Unmarshal(bodyBytes, &respError) 278 | if err != nil { 279 | return false, err 280 | } 281 | 282 | var hasGc bool 283 | 284 | switch respError.Details.Status { 285 | case "ALREADY_REGISTERED", "NOT_ENTITLED": 286 | { 287 | hasGc = false 288 | } 289 | case "DUPLICATE", "NOT_ALLOWED": 290 | { 291 | hasGc = true 292 | } 293 | default: 294 | { 295 | hasGc = false 296 | } 297 | } 298 | 299 | return hasGc, nil 300 | 301 | } 302 | 303 | return false, &RequestError{StatusCode: resp.StatusCode, Err: fmt.Errorf("got status: %v body: %v", resp.Status, string(bodyBytes))} 304 | 305 | } 306 | 307 | // grab information on the availability of name change for this account 308 | func (account *MCaccount) NameChangeInfo() (nameChangeInfoResponse, error) { 309 | req, err := account.AuthenticatedReq("GET", "https://api.minecraftservices.com/minecraft/profile/namechange", nil) 310 | 311 | if err != nil { 312 | return nameChangeInfoResponse{}, err 313 | } 314 | 315 | resp, err := http.DefaultClient.Do(req) 316 | if err != nil { 317 | return nameChangeInfoResponse{}, err 318 | } 319 | defer resp.Body.Close() 320 | 321 | respBody, err := ioutil.ReadAll(resp.Body) 322 | if err != nil { 323 | return nameChangeInfoResponse{}, err 324 | } 325 | 326 | if resp.StatusCode >= 400 { 327 | return nameChangeInfoResponse{ 328 | Changedat: time.Time{}, 329 | Createdat: time.Time{}, 330 | Namechangeallowed: false, 331 | }, &RequestError{ 332 | StatusCode: resp.StatusCode, 333 | Err: errors.New("failed to grab name change info"), 334 | } 335 | } 336 | 337 | var parsedNameChangeInfo nameChangeInfoResponse 338 | 339 | err = json.Unmarshal(respBody, &parsedNameChangeInfo) 340 | 341 | if err != nil { 342 | return nameChangeInfoResponse{}, err 343 | } 344 | 345 | return parsedNameChangeInfo, nil 346 | } 347 | 348 | func (account *MCaccount) ChangeName(username string, changeTime time.Time, createProfile bool) (NameChangeReturn, error) { 349 | 350 | var payload string 351 | if createProfile { 352 | data := fmt.Sprintf(`{"profileName": "%s"}`, username) 353 | payload = fmt.Sprintf( 354 | "POST /minecraft/profile HTTP/1.1\r\n"+ 355 | "Host: api.minecraftservices.com\r\n"+ 356 | "Authorization: Bearer %s\r\n"+ 357 | "Content-Type: application/json\r\n"+ 358 | "Content-Length: %d\r\n"+ 359 | "\r\n"+ 360 | "%s", 361 | account.Bearer, 362 | len(data), 363 | data, 364 | ) 365 | // credit to peet for that ^ 366 | // and credit to tenscape for teaching me how HTTP works lol 367 | } else { 368 | payload = fmt.Sprintf("PUT /minecraft/profile/name/%s HTTP/1.1\r\nHost: api.minecraftservices.com\r\nContent-Length: 0\r\nAuthorization: Bearer %s\r\n\r\n", username, account.Bearer) 369 | // and that 370 | } 371 | 372 | recvd := make([]byte, 4096) 373 | 374 | time.Sleep(time.Until(changeTime) - time.Second*20) 375 | 376 | conn, err := tls.Dial("tcp", "api.minecraftservices.com"+":443", nil) 377 | conn.Write([]byte(payload[:len(payload)-2])) 378 | if err != nil { 379 | return NameChangeReturn{ 380 | Account: MCaccount{}, 381 | Username: username, 382 | ChangedName: false, 383 | StatusCode: 0, 384 | SendTime: time.Time{}, 385 | ReceiveTime: time.Time{}, 386 | }, err 387 | } 388 | 389 | time.Sleep(time.Until(changeTime)) 390 | 391 | conn.Write([]byte(payload[len(payload)-2:])) 392 | sendTime := time.Now() 393 | 394 | conn.Read(recvd) 395 | recvTime := time.Now() 396 | conn.Close() 397 | status, err := strconv.Atoi(string(recvd[9:12])) 398 | 399 | if err != nil { 400 | return NameChangeReturn{ 401 | Account: MCaccount{}, 402 | Username: username, 403 | ChangedName: false, 404 | StatusCode: 0, 405 | SendTime: sendTime, 406 | ReceiveTime: time.Time{}, 407 | }, err 408 | } 409 | 410 | toRet := NameChangeReturn{ 411 | Account: *account, 412 | Username: username, 413 | ChangedName: status < 300, 414 | StatusCode: status, 415 | SendTime: sendTime, 416 | ReceiveTime: recvTime, 417 | } 418 | return toRet, nil 419 | } 420 | 421 | func (account *MCaccount) ChangeSkinFromUrl(url, variant string) error { 422 | body := fmt.Sprintf(`{"url": "%v", "variant": "%v"}`, url, variant) 423 | req, err := account.AuthenticatedReq("POST", "https://api.minecraftservices.com/minecraft/profile/skins", strings.NewReader(body)) 424 | if err != nil { 425 | return err 426 | } 427 | 428 | req.Header.Set("Content-Type", "application/json") 429 | 430 | resp, err := http.DefaultClient.Do(req) 431 | 432 | if err != nil { 433 | return err 434 | } 435 | 436 | if resp.StatusCode != 200 { 437 | return errors.New("unauthorized") 438 | } 439 | 440 | return nil 441 | } 442 | -------------------------------------------------------------------------------- /account_test.go: -------------------------------------------------------------------------------- 1 | package mcgo 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestPrename(t *testing.T) { 11 | bearer := os.Getenv("BEARER") 12 | if bearer == "" { 13 | bearer = "TestToken" 14 | } 15 | acc := MCaccount{Bearer: bearer} 16 | 17 | nameChangeRet, err := acc.ChangeName("test", time.Now().Add(time.Second*1), true) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | fmt.Println(nameChangeRet.ChangedName) 22 | fmt.Println(nameChangeRet.SendTime) 23 | fmt.Println(nameChangeRet.ReceiveTime) 24 | fmt.Println(nameChangeRet.StatusCode) 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kqzz/mcgo 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kqzz/mcgo/df87f11ac4cbebcd8f6a347c47a7ca1cb3c137b5/go.sum -------------------------------------------------------------------------------- /msa.go: -------------------------------------------------------------------------------- 1 | package mcgo 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/http/cookiejar" 12 | "net/url" 13 | "regexp" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // Old implementation of msa (oauth2, not needed for this pkg) available @ https://gist.github.com/Kqzz/02d33be5868bb4d8bb2a3aa03f72052c 19 | 20 | type xBLSignInBody struct { 21 | Properties struct { 22 | Authmethod string `json:"AuthMethod"` 23 | Sitename string `json:"SiteName"` 24 | Rpsticket string `json:"RpsTicket"` 25 | } `json:"Properties"` 26 | Relyingparty string `json:"RelyingParty"` 27 | Tokentype string `json:"TokenType"` 28 | } 29 | 30 | type XBLSignInResp struct { 31 | Issueinstant time.Time `json:"IssueInstant"` 32 | Notafter time.Time `json:"NotAfter"` 33 | Token string `json:"Token"` 34 | Displayclaims struct { 35 | Xui []struct { 36 | Uhs string `json:"uhs"` 37 | } `json:"xui"` 38 | } `json:"DisplayClaims"` 39 | } 40 | 41 | type xSTSPostBody struct { 42 | Properties struct { 43 | Sandboxid string `json:"SandboxId"` 44 | Usertokens []string `json:"UserTokens"` 45 | } `json:"Properties"` 46 | Relyingparty string `json:"RelyingParty"` 47 | Tokentype string `json:"TokenType"` 48 | } 49 | 50 | type xSTSAuthorizeResponse struct { 51 | Issueinstant time.Time `json:"IssueInstant"` 52 | Notafter time.Time `json:"NotAfter"` 53 | Token string `json:"Token"` 54 | Displayclaims struct { 55 | Xui []struct { 56 | Uhs string `json:"uhs"` 57 | } `json:"xui"` 58 | } `json:"DisplayClaims"` 59 | } 60 | 61 | type xSTSAuthorizeResponseFail struct { 62 | Identity string `json:"Identity"` 63 | Xerr int64 `json:"XErr"` 64 | Message string `json:"Message"` 65 | Redirect string `json:"Redirect"` 66 | } 67 | 68 | type msGetMojangbearerBody struct { 69 | Identitytoken string `json:"identityToken"` 70 | Ensurelegacyenabled bool `json:"ensureLegacyEnabled"` 71 | } 72 | 73 | type msGetMojangBearerResponse struct { 74 | TokenType string `json:"token_type"` 75 | ExpiresIn int `json:"expires_in"` 76 | Scope string `json:"scope"` 77 | AccessToken string `json:"access_token"` 78 | RefreshToken string `json:"refresh_token"` 79 | UserID string `json:"user_id"` 80 | Foci string `json:"foci"` 81 | } 82 | 83 | func (account *MCaccount) MicrosoftAuthenticate() error { 84 | jar, err := cookiejar.New(nil) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | tr := &http.Transport{ 90 | TLSClientConfig: &tls.Config{ 91 | Renegotiation: tls.RenegotiateOnceAsClient, 92 | InsecureSkipVerify: true}, 93 | } 94 | 95 | var redirect string 96 | client := &http.Client{ 97 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 98 | redirect = req.URL.String() 99 | return nil 100 | }, 101 | Jar: jar, 102 | Transport: tr, 103 | } 104 | // Grab value and urlpost 105 | valRegex := regexp.MustCompile(`value="(.+?)"`) 106 | urlPostRegex := regexp.MustCompile(`urlPost:'(.+?)'`) 107 | 108 | resp, err := client.Get("https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en") 109 | 110 | if err != nil { 111 | return err 112 | } 113 | 114 | respBytes, err := ioutil.ReadAll(resp.Body) 115 | 116 | if err != nil { 117 | return err 118 | } 119 | 120 | // respString := string(respBytes) 121 | 122 | value := string(valRegex.FindAllSubmatch(respBytes, -1)[0][1]) 123 | urlPost := string(urlPostRegex.FindAllSubmatch(respBytes, -1)[0][1]) 124 | 125 | // Sign in to microsoft 126 | 127 | emailEncoded := url.QueryEscape(account.Email) 128 | passwordEncoded := url.QueryEscape(account.Password) 129 | valueEncoded := url.QueryEscape(value) 130 | 131 | body := []byte(fmt.Sprintf("login=%v&loginfmt=%v&passwd=%v&PPFT=%v", emailEncoded, emailEncoded, passwordEncoded, valueEncoded)) 132 | 133 | req, err := http.NewRequest("POST", urlPost, bytes.NewReader(body)) 134 | 135 | if err != nil { 136 | return err 137 | } 138 | 139 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 140 | 141 | resp, err = client.Do(req) 142 | 143 | if err != nil { 144 | return err 145 | } 146 | 147 | defer resp.Body.Close() 148 | 149 | if resp.Request.URL.String() == urlPost && strings.Contains(resp.Request.URL.String(), "access_token") { 150 | return errors.New("failed microsoft authentication, invalid credentials") 151 | } 152 | 153 | respBytes, err = ioutil.ReadAll(resp.Body) 154 | 155 | if err != nil { 156 | return err 157 | } 158 | 159 | respStr := string(respBytes) 160 | 161 | if strings.Contains(respStr, "Sign in to") { 162 | return errors.New("invalid credentials") 163 | } 164 | 165 | if strings.Contains(respStr, "Help us protect your account") { 166 | return errors.New("2fa is enabled, which is not supported now") 167 | } 168 | 169 | if !strings.Contains(redirect, "access_token") || redirect == urlPost { 170 | return errors.New("invalid credentials") 171 | } 172 | 173 | params := strings.Split(redirect, "#")[1] 174 | 175 | loginData := map[string]string{} 176 | 177 | for _, item := range strings.Split(params, "&") { 178 | itemSplit := strings.Split(item, "=") 179 | v, _ := url.QueryUnescape(itemSplit[1]) 180 | loginData[itemSplit[0]] = v 181 | } 182 | 183 | data := xBLSignInBody{ 184 | Properties: struct { 185 | Authmethod string "json:\"AuthMethod\"" 186 | Sitename string "json:\"SiteName\"" 187 | Rpsticket string "json:\"RpsTicket\"" 188 | }{ 189 | Authmethod: "RPS", 190 | Sitename: "user.auth.xboxlive.com", 191 | Rpsticket: loginData["access_token"], 192 | }, 193 | Relyingparty: "http://auth.xboxlive.com", 194 | Tokentype: "JWT", 195 | } 196 | 197 | encodedBody, err := json.Marshal(data) 198 | if err != nil { 199 | return err 200 | } 201 | req, err = http.NewRequest("POST", "https://user.auth.xboxlive.com/user/authenticate", bytes.NewReader(encodedBody)) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | req.Header.Set("Content-Type", "application/json") 207 | req.Header.Set("Accept", "application/json") 208 | req.Header.Set("x-xbl-contract-version", "1") 209 | 210 | resp, err = client.Do(req) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | defer resp.Body.Close() 216 | 217 | respBodyBytes, err := ioutil.ReadAll(resp.Body) 218 | if resp.StatusCode == 400 { 219 | return errors.New("invalid Rpsticket field probably") 220 | } 221 | 222 | if err != nil { 223 | return err 224 | } 225 | 226 | var respBody XBLSignInResp 227 | 228 | json.Unmarshal(respBodyBytes, &respBody) 229 | 230 | uhs := respBody.Displayclaims.Xui[0].Uhs 231 | XBLToken := respBody.Token 232 | 233 | xstsBody := xSTSPostBody{ 234 | Properties: struct { 235 | Sandboxid string "json:\"SandboxId\"" 236 | Usertokens []string "json:\"UserTokens\"" 237 | }{ 238 | Sandboxid: "RETAIL", 239 | Usertokens: []string{ 240 | XBLToken, 241 | }, 242 | }, 243 | Relyingparty: "rp://api.minecraftservices.com/", 244 | Tokentype: "JWT", 245 | } 246 | 247 | encodedXstsBody, err := json.Marshal(xstsBody) 248 | if err != nil { 249 | return err 250 | } 251 | req, err = http.NewRequest("POST", "https://xsts.auth.xboxlive.com/xsts/authorize", bytes.NewReader(encodedXstsBody)) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | resp, err = client.Do(req) 257 | 258 | if err != nil { 259 | return err 260 | } 261 | 262 | respBodyBytes, err = ioutil.ReadAll(resp.Body) 263 | 264 | if err != nil { 265 | return err 266 | } 267 | 268 | if resp.StatusCode == 401 { 269 | var authorizeXstsFail xSTSAuthorizeResponseFail 270 | json.Unmarshal(respBodyBytes, &authorizeXstsFail) 271 | switch authorizeXstsFail.Xerr { 272 | case 2148916238: 273 | { 274 | return errors.New("microsoft account belongs to someone under 18! add to family for this to work") 275 | } 276 | case 2148916233: 277 | { 278 | return errors.New("you have no xbox account! Sign up for one to continue") 279 | } 280 | default: 281 | { 282 | return fmt.Errorf("got error code %v when trying to authorize XSTS token", authorizeXstsFail.Xerr) 283 | } 284 | } 285 | } 286 | 287 | var xstsAuthorizeResp xSTSAuthorizeResponse 288 | json.Unmarshal(respBodyBytes, &xstsAuthorizeResp) 289 | 290 | xstsToken := xstsAuthorizeResp.Token 291 | 292 | mojangBearerBody := msGetMojangbearerBody{ 293 | Identitytoken: "XBL3.0 x=" + uhs + ";" + xstsToken, 294 | Ensurelegacyenabled: true, 295 | } 296 | 297 | mojangBearerBodyEncoded, err := json.Marshal(mojangBearerBody) 298 | 299 | if err != nil { 300 | return err 301 | } 302 | 303 | req, err = http.NewRequest("POST", "https://api.minecraftservices.com/authentication/login_with_xbox", bytes.NewReader(mojangBearerBodyEncoded)) 304 | 305 | req.Header.Set("Content-Type", "application/json") 306 | 307 | if err != nil { 308 | return err 309 | } 310 | 311 | resp, err = client.Do(req) 312 | if err != nil { 313 | return err 314 | } 315 | 316 | mcBearerResponseBytes, err := ioutil.ReadAll(resp.Body) 317 | 318 | if err != nil { 319 | return err 320 | } 321 | 322 | var mcBearerResp msGetMojangBearerResponse 323 | 324 | json.Unmarshal(mcBearerResponseBytes, &mcBearerResp) 325 | 326 | account.Bearer = mcBearerResp.AccessToken 327 | 328 | return nil 329 | } 330 | -------------------------------------------------------------------------------- /msa_ouath_device_flow.go: -------------------------------------------------------------------------------- 1 | package mcgo 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/http/cookiejar" 12 | "time" 13 | ) 14 | 15 | /* 16 | Client ID is 648b1790-3c45-4745-bd7b-d9e828433655, applet name is mcgo Library Authentication 17 | 18 | Flow is as follows: 19 | POST https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode 20 | ?client_id=648b1790-3c45-4745-bd7b-d9e828433655 21 | &scope=XboxLive.signin 22 | 23 | Put user instructions from response.message in console. 24 | 25 | POST https:// 26 | ?grant_type=urn:ietf:params:oauth:grant-type:device_code 27 | &client_id=648b1790-3c45-4745-bd7b-d9e828433655 28 | &device_code={respone.device_code} 29 | 30 | once every response.interval seconds until expires_in timeout or successful poll. 31 | 32 | Errors to properly handle in response.error: 33 | authorization_pending - keep waiting. user isn't done. 34 | authorization_declined - user declined auth, fail to authenticate. 35 | bad_verification_code - this one should request a bug report on github. won't happen normally 36 | expired_token - stop polling, fail to authenticate. user took too long.const 37 | 38 | Fields to use once response.error is nil: 39 | access_token - use this with https://user.auth.xboxlive.com/user/authenticate to get xsts done. 40 | expires_in - if implemented, should request reauthentication once expired. 41 | 42 | */ 43 | 44 | // we only take the useful fields here. 45 | 46 | type msDeviceInitResponse struct { 47 | Message string `json:"message"` 48 | Interval int `json:"interval"` 49 | DeviceCode string `json:"device_code"` 50 | } 51 | 52 | type msErrorPollResponse struct { 53 | Error string `json:"error"` 54 | } 55 | 56 | type msSuccessPollResponse struct { 57 | AccessToken string `json:"access_token"` 58 | } 59 | 60 | // due to the nature of these requests, the client id may be swapped out for another and work just fine assuming AD is configured properly 61 | 62 | const client_id = "648b1790-3c45-4745-bd7b-d9e828433655" 63 | 64 | // types in msa.go are used here as well. 65 | 66 | func authWithToken(account *MCaccount, access_token_from_ms string) error { 67 | jar, err := cookiejar.New(nil) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | tr := &http.Transport{ 73 | TLSClientConfig: &tls.Config{ 74 | Renegotiation: tls.RenegotiateOnceAsClient, 75 | InsecureSkipVerify: true}, 76 | } 77 | 78 | client := &http.Client{ 79 | Jar: jar, 80 | Transport: tr, 81 | } 82 | data := xBLSignInBody{ 83 | Properties: struct { 84 | Authmethod string "json:\"AuthMethod\"" 85 | Sitename string "json:\"SiteName\"" 86 | Rpsticket string "json:\"RpsTicket\"" 87 | }{ 88 | Authmethod: "RPS", 89 | Sitename: "user.auth.xboxlive.com", 90 | Rpsticket: "d=" + access_token_from_ms, 91 | }, 92 | Relyingparty: "http://auth.xboxlive.com", 93 | Tokentype: "JWT", 94 | } 95 | 96 | encodedBody, err := json.Marshal(data) 97 | if err != nil { 98 | return err 99 | } 100 | req, err := http.NewRequest("POST", "https://user.auth.xboxlive.com/user/authenticate", bytes.NewReader(encodedBody)) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | req.Header.Set("Content-Type", "application/json") 106 | req.Header.Set("Accept", "application/json") 107 | req.Header.Set("x-xbl-contract-version", "1") 108 | 109 | resp, err := client.Do(req) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | defer resp.Body.Close() 115 | 116 | respBodyBytes, err := ioutil.ReadAll(resp.Body) 117 | if resp.StatusCode == 400 { 118 | return errors.New("invalid Rpsticket field probably") 119 | } 120 | 121 | if err != nil { 122 | return err 123 | } 124 | 125 | var respBody XBLSignInResp 126 | 127 | json.Unmarshal(respBodyBytes, &respBody) 128 | 129 | uhs := respBody.Displayclaims.Xui[0].Uhs 130 | XBLToken := respBody.Token 131 | 132 | xstsBody := xSTSPostBody{ 133 | Properties: struct { 134 | Sandboxid string "json:\"SandboxId\"" 135 | Usertokens []string "json:\"UserTokens\"" 136 | }{ 137 | Sandboxid: "RETAIL", 138 | Usertokens: []string{ 139 | XBLToken, 140 | }, 141 | }, 142 | Relyingparty: "rp://api.minecraftservices.com/", 143 | Tokentype: "JWT", 144 | } 145 | 146 | encodedXstsBody, err := json.Marshal(xstsBody) 147 | if err != nil { 148 | return err 149 | } 150 | req, err = http.NewRequest("POST", "https://xsts.auth.xboxlive.com/xsts/authorize", bytes.NewReader(encodedXstsBody)) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | resp, err = client.Do(req) 156 | 157 | if err != nil { 158 | return err 159 | } 160 | 161 | respBodyBytes, err = ioutil.ReadAll(resp.Body) 162 | 163 | if err != nil { 164 | return err 165 | } 166 | 167 | if resp.StatusCode == 401 { 168 | var authorizeXstsFail xSTSAuthorizeResponseFail 169 | json.Unmarshal(respBodyBytes, &authorizeXstsFail) 170 | switch authorizeXstsFail.Xerr { 171 | case 2148916238: 172 | { 173 | return errors.New("microsoft account belongs to someone under 18! add to family for this to work") 174 | } 175 | case 2148916233: 176 | { 177 | return errors.New("you have no xbox account! Sign up for one to continue") 178 | } 179 | default: 180 | { 181 | return fmt.Errorf("got error code %v when trying to authorize XSTS token", authorizeXstsFail.Xerr) 182 | } 183 | } 184 | } 185 | 186 | var xstsAuthorizeResp xSTSAuthorizeResponse 187 | json.Unmarshal(respBodyBytes, &xstsAuthorizeResp) 188 | 189 | xstsToken := xstsAuthorizeResp.Token 190 | 191 | mojangBearerBody := msGetMojangbearerBody{ 192 | Identitytoken: "XBL3.0 x=" + uhs + ";" + xstsToken, 193 | Ensurelegacyenabled: true, 194 | } 195 | 196 | mojangBearerBodyEncoded, err := json.Marshal(mojangBearerBody) 197 | 198 | if err != nil { 199 | return err 200 | } 201 | 202 | req, err = http.NewRequest("POST", "https://api.minecraftservices.com/authentication/login_with_xbox", bytes.NewReader(mojangBearerBodyEncoded)) 203 | 204 | req.Header.Set("Content-Type", "application/json") 205 | 206 | if err != nil { 207 | return err 208 | } 209 | 210 | resp, err = client.Do(req) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | mcBearerResponseBytes, err := ioutil.ReadAll(resp.Body) 216 | 217 | if err != nil { 218 | return err 219 | } 220 | 221 | var mcBearerResp msGetMojangBearerResponse 222 | 223 | json.Unmarshal(mcBearerResponseBytes, &mcBearerResp) 224 | 225 | account.Bearer = mcBearerResp.AccessToken 226 | 227 | return nil 228 | } 229 | 230 | func (account *MCaccount) InitAuthFlow() error { 231 | jar, err := cookiejar.New(nil) 232 | if err != nil { 233 | return err 234 | } 235 | 236 | tr := &http.Transport{ 237 | TLSClientConfig: &tls.Config{ 238 | Renegotiation: tls.RenegotiateOnceAsClient, 239 | InsecureSkipVerify: true}, 240 | } 241 | 242 | client := &http.Client{ 243 | Jar: jar, 244 | Transport: tr, 245 | } 246 | 247 | reqParams := fmt.Sprintf("client_id=%s&scope=XboxLive.signin", client_id) 248 | 249 | req, _ := http.NewRequest("POST", "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", bytes.NewBuffer([]byte(reqParams))) 250 | 251 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 252 | 253 | resp, err := client.Do(req) 254 | if err != nil { 255 | return err 256 | } 257 | defer resp.Body.Close() 258 | respbytes, err := ioutil.ReadAll(resp.Body) 259 | if resp.StatusCode != 200 { 260 | return errors.New("non-200 status on devicecode post") 261 | } 262 | 263 | if err != nil { 264 | return err 265 | } 266 | 267 | var respObj msDeviceInitResponse 268 | err = json.Unmarshal(respbytes, &respObj) 269 | if err != nil { 270 | return err 271 | } 272 | fmt.Printf("auth for mc account: %s\n", respObj.Message) 273 | 274 | return pollEndpoint(account, respObj.DeviceCode, respObj.Interval) 275 | } 276 | 277 | func pollEndpoint(account *MCaccount, device_code string, interval int) error { 278 | 279 | sleepDuration := time.Second * time.Duration(interval) 280 | jar, err := cookiejar.New(nil) 281 | if err != nil { 282 | return err 283 | } 284 | 285 | tr := &http.Transport{ 286 | TLSClientConfig: &tls.Config{ 287 | Renegotiation: tls.RenegotiateOnceAsClient, 288 | InsecureSkipVerify: true}, 289 | } 290 | 291 | client := &http.Client{ 292 | Jar: jar, 293 | Transport: tr, 294 | } 295 | 296 | reqParams := fmt.Sprintf("grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=%s&client_id=%s", device_code, client_id) 297 | for { 298 | time.Sleep(sleepDuration) 299 | req, err := http.NewRequest("POST", "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", bytes.NewBuffer([]byte(reqParams))) 300 | if err != nil { 301 | return err 302 | } 303 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 304 | resp, err := client.Do(req) 305 | if err != nil { 306 | return err 307 | } 308 | defer resp.Body.Close() 309 | byteRes, err := ioutil.ReadAll(resp.Body) 310 | if err != nil { 311 | return err 312 | } 313 | 314 | if resp.StatusCode == 400 { 315 | var r msErrorPollResponse 316 | err = json.Unmarshal(byteRes, &r) 317 | if err != nil { 318 | return err 319 | } 320 | switch r.Error { 321 | case "authorization_pending": 322 | continue 323 | case "authorization_declined", "expired_token": 324 | return errors.New("authorization failed. cannot continue") 325 | default: 326 | return errors.New("unknown state on 400 status") 327 | } 328 | } else if resp.StatusCode == 200 { 329 | var r msSuccessPollResponse 330 | err = json.Unmarshal(byteRes, &r) 331 | if err != nil { 332 | return err 333 | } 334 | return authWithToken(account, r.AccessToken) 335 | } else { 336 | return errors.New("status code response not 200 or 400") 337 | } 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /msa_test.go: -------------------------------------------------------------------------------- 1 | package mcgo 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestMsa(t *testing.T) { 9 | email := os.Getenv("MS_EMAIL") 10 | pass := os.Getenv("MS_PASSWORD") 11 | acc := MCaccount{ 12 | Email: email, 13 | Password: pass, 14 | } 15 | if err := acc.MicrosoftAuthenticate(); err != nil || acc.Bearer == "" { 16 | t.Fatal(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package mcgo 2 | 3 | import "time" 4 | 5 | type AccType string 6 | 7 | const ( 8 | Ms AccType = "ms" 9 | Mj AccType = "mj" 10 | MsPr AccType = "mspr" 11 | ) 12 | 13 | /// RETURNS / API-FACING /// 14 | 15 | // TODO: Use RequestError for status-code-related errors 16 | type RequestError struct { 17 | StatusCode int 18 | Err error 19 | } 20 | 21 | func (r *RequestError) Error() string { 22 | return r.Err.Error() 23 | } 24 | 25 | // Holds name change information for an account, the time the current account was created, it's name was most recently changed, and if it can currently change its name. 26 | 27 | type NameChangeReturn struct { 28 | Account MCaccount 29 | Username string 30 | ChangedName bool 31 | StatusCode int 32 | SendTime time.Time 33 | ReceiveTime time.Time 34 | } 35 | 36 | // represents a minecraft account 37 | type MCaccount struct { 38 | Email string 39 | Password string 40 | SecurityQuestions []SqAnswer 41 | SecurityAnswers []string 42 | Bearer string 43 | UUID string 44 | Username string 45 | Type AccType 46 | Authenticated bool 47 | } 48 | 49 | /// HTTP RESPONSE BODIES /// 50 | 51 | type nameChangeInfoResponse struct { 52 | Changedat time.Time `json:"changedAt"` 53 | Createdat time.Time `json:"createdAt"` 54 | Namechangeallowed bool `json:"nameChangeAllowed"` 55 | } 56 | 57 | type hasGcAppliedResp struct { 58 | Path string `json:"path"` 59 | ErrorType string `json:"errorType"` 60 | Error string `json:"error"` 61 | ErrorMessage string `json:"errorMessage"` 62 | DeveloperMessage string `json:"developerMessage"` 63 | Details struct { 64 | Status string `json:"status"` 65 | } `json:"details"` 66 | } 67 | 68 | type SqAnswer struct { 69 | Answer struct { 70 | ID int `json:"id"` 71 | } `json:"answer"` 72 | Question struct { 73 | ID int `json:"id"` 74 | Question string `json:"question"` 75 | } `json:"question"` 76 | } 77 | 78 | type accInfoResponse struct { 79 | ID string `json:"id"` 80 | Name string `json:"name"` 81 | } 82 | 83 | type authenticateReqResp struct { 84 | User struct { 85 | Properties []struct { 86 | Name string `json:"name"` 87 | Value string `json:"value"` 88 | } `json:"properties"` 89 | Username string `json:"username"` 90 | ID string `json:"id"` 91 | } `json:"user"` 92 | Accesstoken string `json:"accessToken"` 93 | Clienttoken string `json:"clientToken"` 94 | } 95 | 96 | /// SEND BODIES /// 97 | type submitPostJson struct { 98 | ID int `json:"id"` 99 | Answer string `json:"answer"` 100 | } 101 | -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git tag $1 3 | git push origin $1 4 | GOPROXY=proxy.golang.org go list -m github.com/Kqzz/mcgo@$1 5 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package mcgo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type Droptime struct { 11 | Droptime time.Time 12 | Username string 13 | } 14 | 15 | // https://stackoverflow.com/a/68240817/13312615 16 | func SameErrorMessage(err, target error) bool { 17 | if target == nil || err == nil { 18 | return err == target 19 | } 20 | return err.Error() == target.Error() 21 | } 22 | 23 | func NameAvailability(username string) (string, error) { 24 | resp, err := http.Get(fmt.Sprintf("https://api.mojang.com/user/profile/agent/minecraft/name/%v", username)) 25 | 26 | if err != nil { 27 | return "", err 28 | } 29 | 30 | if resp.StatusCode == 200 { 31 | return "claimed", nil 32 | } 33 | 34 | if resp.StatusCode == 400 { 35 | return "invalid", nil 36 | } 37 | 38 | if resp.StatusCode == 429 { 39 | return "", errors.New("mojang API ratelimit reached") 40 | } 41 | 42 | return "", fmt.Errorf("this should not be possible! | Got status %v on request for name availability", resp.StatusCode) 43 | } 44 | --------------------------------------------------------------------------------