├── go.sum ├── go.mod ├── README.md ├── LICENSE └── auth.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/maxsupermanhd/go-mc-ms-auth 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-mc-ms-auth 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/maxsupermanhd/go-mc-ms-auth.svg)](https://pkg.go.dev/github.com/maxsupermanhd/go-mc-ms-auth) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/maxsupermanhd/go-mc-ms-auth)](https://goreportcard.com/report/github.com/maxsupermanhd/go-mc-ms-auth) 5 | 6 | Require Go version: 1.16 7 | 8 | Package for getting Minecraft credentials from Microsoft. 9 | 10 | Example: 11 | 12 | ```go 13 | mauth, err := GMMAuth.GetMCcredentials("./auth.cache", "88650e7e-efee-4857-b9a9-cf580a00ef43") 14 | if err != nil { 15 | log.Print(err) 16 | return 17 | } 18 | log.Print("Authenticated as ", mauth.Name, " (", mauth.UUID, ")") 19 | mcClient := bot.NewClient() 20 | mcClient.Auth = mauth 21 | // client can go brrr 22 | ``` 23 | 24 | What is AzureAppId and how to get one: see issue #2\ 25 | TLDR: you can use this one: `88650e7e-efee-4857-b9a9-cf580a00ef43` 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Maxim Zhuchkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package GMMAuth 2 | 3 | // Go MC MIcroSoft Auth 4 | 5 | import ( 6 | "bytes" 7 | "crypto/tls" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "log" 12 | "net/http" 13 | "net/url" 14 | "os" 15 | "strconv" 16 | "time" 17 | ) 18 | 19 | // BotAuth is a literal replacement of Tnze's bot.Auth 20 | // One day he will go nuts and do something with it 21 | // and I don't feel like fixing it after him 22 | type BotAuth struct { 23 | Name string 24 | UUID string 25 | AsTk string 26 | } 27 | 28 | // MSauth holds Microsoft auth credentials 29 | type MSauth struct { 30 | AccessToken string 31 | ExpiresAfter int64 32 | RefreshToken string 33 | } 34 | 35 | // AzureClientIDEnvVar Used to lookup Azure client id via os.Getenv if cid is not passed 36 | const AzureClientIDEnvVar = "AzureClientID" 37 | 38 | // CheckRefreshMS Checks MSauth for expired token and refreshes if needed 39 | func CheckRefreshMS(auth *MSauth, cid string) error { 40 | if auth.ExpiresAfter > time.Now().Unix() { 41 | return nil 42 | } 43 | if cid == "" { 44 | cid = os.Getenv(AzureClientIDEnvVar) 45 | } 46 | if auth.RefreshToken == "" { 47 | return errors.New("MS access token expired and no refresh token provided") 48 | } 49 | MSdata := url.Values{ 50 | "client_id": {cid}, 51 | // "client_secret": {os.Getenv("AzureSecret")}, 52 | "refresh_token": {auth.RefreshToken}, 53 | "grant_type": {"refresh_token"}, 54 | "redirect_uri": {"https://login.microsoftonline.com/common/oauth2/nativeclient"}, 55 | } 56 | MSresp, err := http.PostForm("https://login.live.com/oauth20_token.srf", MSdata) 57 | if err != nil { 58 | return err 59 | } 60 | var MSres map[string]interface{} 61 | json.NewDecoder(MSresp.Body).Decode(&MSres) 62 | MSresp.Body.Close() 63 | if MSresp.StatusCode != 200 { 64 | return fmt.Errorf("MS refresh attempt answered not HTTP200! Instead got %s and following json: %#v", MSresp.Status, MSres) 65 | } 66 | MSaccessToken, ok := MSres["access_token"].(string) 67 | if !ok { 68 | return errors.New("access_token not found in response") 69 | } 70 | auth.AccessToken = MSaccessToken 71 | MSrefreshToken, ok := MSres["refresh_token"].(string) 72 | if !ok { 73 | return errors.New("refresh_token not found in response") 74 | } 75 | auth.RefreshToken = MSrefreshToken 76 | MSexpireSeconds, ok := MSres["expires_in"].(float64) 77 | if !ok { 78 | return errors.New("expires_in not found in response") 79 | } 80 | auth.ExpiresAfter = time.Now().Unix() + int64(MSexpireSeconds) 81 | return nil 82 | } 83 | 84 | // AuthMSdevice Attempts to authorize user via device flow. Will block thread until gets error, timeout or actual authorization 85 | func AuthMSdevice(cid string) (MSauth, error) { 86 | var auth MSauth 87 | if cid == "" { 88 | cid = os.Getenv(AzureClientIDEnvVar) 89 | } 90 | DeviceResp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", url.Values{ 91 | "client_id": {cid}, 92 | "scope": {`XboxLive.signin offline_access`}, 93 | }) 94 | if err != nil { 95 | return auth, err 96 | } 97 | var DeviceRes map[string]interface{} 98 | json.NewDecoder(DeviceResp.Body).Decode(&DeviceRes) 99 | DeviceResp.Body.Close() 100 | if DeviceResp.StatusCode != 200 { 101 | return auth, fmt.Errorf("MS device request answered not HTTP200! Instead got %s and following json: %#v", DeviceResp.Status, DeviceRes) 102 | } 103 | DeviceCode, ok := DeviceRes["device_code"].(string) 104 | if !ok { 105 | return auth, errors.New("device code not found in response") 106 | } 107 | UserCode, ok := DeviceRes["user_code"].(string) 108 | if !ok { 109 | return auth, errors.New("user code not found in response") 110 | } 111 | log.Print("User code: ", UserCode) 112 | VerificationURI, ok := DeviceRes["verification_uri"].(string) 113 | if !ok { 114 | return auth, errors.New("verification URI not found in response") 115 | } 116 | log.Print("Verification URI: ", VerificationURI) 117 | ExpiresIn, ok := DeviceRes["expires_in"].(float64) 118 | if !ok { 119 | return auth, errors.New("expires_in not found in response") 120 | } 121 | log.Print("Expires in: ", ExpiresIn, " seconds") 122 | PoolInterval, ok := DeviceRes["interval"].(float64) 123 | if !ok { 124 | return auth, errors.New("pooling interval not found in response") 125 | } 126 | UserMessage, ok := DeviceRes["message"].(string) 127 | if !ok { 128 | return auth, errors.New("pooling interval not found in response") 129 | } 130 | log.Println(UserMessage) 131 | time.Sleep(4 * time.Second) 132 | 133 | for { 134 | time.Sleep(time.Duration(int(PoolInterval)+1) * time.Second) 135 | CodeResp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{ 136 | "client_id": {cid}, 137 | "scope": {"XboxLive.signin offline_access"}, 138 | "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, 139 | "device_code": {DeviceCode}, 140 | }) 141 | if err != nil { 142 | return auth, err 143 | } 144 | var CodeRes map[string]interface{} 145 | json.NewDecoder(CodeResp.Body).Decode(&CodeRes) 146 | CodeResp.Body.Close() 147 | if CodeResp.StatusCode == 400 { 148 | PoolError, ok := CodeRes["error"].(string) 149 | if !ok { 150 | return auth, fmt.Errorf("while pooling token got this unknown json: %#v", CodeRes) 151 | } 152 | if PoolError == "authorization_pending" { 153 | continue 154 | } 155 | if PoolError == "authorization_declined" { 156 | return auth, errors.New("user declined authorization") 157 | } 158 | if PoolError == "expired_token" { 159 | return auth, errors.New("turns out " + strconv.Itoa(int(PoolInterval)) + " seconds is not enough to authorize user, go faster ma monkey") 160 | } 161 | if PoolError == "invalid_grant" { 162 | return auth, errors.New("while pooling token got invalid_grant error: " + CodeRes["error_description"].(string)) 163 | } 164 | } else if CodeResp.StatusCode == 200 { 165 | MSaccessToken, ok := CodeRes["access_token"].(string) 166 | if !ok { 167 | return auth, errors.New("access_token not found in response") 168 | } 169 | auth.AccessToken = MSaccessToken 170 | MSrefreshToken, ok := CodeRes["refresh_token"].(string) 171 | if !ok { 172 | return auth, errors.New("refresh_token not found in response") 173 | } 174 | auth.RefreshToken = MSrefreshToken 175 | MSexpireSeconds, ok := CodeRes["expires_in"].(float64) 176 | if !ok { 177 | return auth, errors.New("expires_in not found in response") 178 | } 179 | auth.ExpiresAfter = time.Now().Unix() + int64(MSexpireSeconds) 180 | return auth, nil 181 | } else { 182 | return auth, fmt.Errorf("MS answered not HTTP200! Instead got %s and following json: %#v", CodeResp.Status, CodeRes) 183 | } 184 | } 185 | 186 | } 187 | 188 | // AuthMSCode Attempts to authorize user via user code (default browser flow) 189 | func AuthMSCode(code string, cid string) (MSauth, error) { 190 | var auth MSauth 191 | if cid == "" { 192 | cid = os.Getenv(AzureClientIDEnvVar) 193 | } 194 | MSdata := url.Values{ 195 | "client_id": {cid}, 196 | // "client_secret": {os.Getenv("AzureSecret")}, 197 | "code": {code}, 198 | "grant_type": {"authorization_code"}, 199 | "redirect_uri": {"https://login.microsoftonline.com/common/oauth2/nativeclient"}, 200 | } 201 | MSresp, err := http.PostForm("https://login.live.com/oauth20_token.srf", MSdata) 202 | if err != nil { 203 | return auth, err 204 | } 205 | var MSres map[string]interface{} 206 | json.NewDecoder(MSresp.Body).Decode(&MSres) 207 | MSresp.Body.Close() 208 | if MSresp.StatusCode != 200 { 209 | return auth, fmt.Errorf("MS answered not HTTP200! Instead got %s and following json: %#v", MSresp.Status, MSres) 210 | } 211 | MSaccessToken, ok := MSres["access_token"].(string) 212 | if !ok { 213 | return auth, errors.New("access_token not found in response") 214 | } 215 | auth.AccessToken = MSaccessToken 216 | MSrefreshToken, ok := MSres["refresh_token"].(string) 217 | if !ok { 218 | return auth, errors.New("refresh_token not found in response") 219 | } 220 | auth.RefreshToken = MSrefreshToken 221 | MSexpireSeconds, ok := MSres["expires_in"].(float64) 222 | if !ok { 223 | return auth, errors.New("expires_in not found in response") 224 | } 225 | auth.ExpiresAfter = time.Now().Unix() + int64(MSexpireSeconds) 226 | return auth, nil 227 | } 228 | 229 | // AuthXBL Gets XBox Live token from Microsoft token 230 | func AuthXBL(MStoken string) (string, error) { 231 | XBLdataMap := map[string]interface{}{ 232 | "Properties": map[string]interface{}{ 233 | "AuthMethod": "RPS", 234 | "SiteName": "user.auth.xboxlive.com", 235 | "RpsTicket": "d=" + MStoken, 236 | }, 237 | "RelyingParty": "http://auth.xboxlive.com", 238 | "TokenType": "JWT", 239 | } 240 | XBLdata, err := json.Marshal(XBLdataMap) 241 | if err != nil { 242 | return "", err 243 | } 244 | XBLreq, err := http.NewRequest(http.MethodPost, "https://user.auth.xboxlive.com/user/authenticate", bytes.NewBuffer(XBLdata)) 245 | if err != nil { 246 | return "", err 247 | } 248 | XBLreq.Header.Set("Content-Type", "application/json") 249 | XBLreq.Header.Set("Accept", "application/json") 250 | transport := &http.Transport{ 251 | TLSClientConfig: &tls.Config{ 252 | Renegotiation: tls.RenegotiateOnceAsClient, 253 | InsecureSkipVerify: true, 254 | }, 255 | } 256 | client := &http.Client{ 257 | Timeout: 5 * time.Second, 258 | Transport: transport, 259 | } 260 | XBLresp, err := client.Do(XBLreq) 261 | if err != nil { 262 | return "", err 263 | } 264 | var XBLres map[string]interface{} 265 | json.NewDecoder(XBLresp.Body).Decode(&XBLres) 266 | XBLresp.Body.Close() 267 | if XBLresp.StatusCode != 200 { 268 | return "", fmt.Errorf("XBL answered not HTTP200! Instead got %s and following json: %#v", XBLresp.Status, XBLres) 269 | } 270 | XBLtoken, ok := XBLres["Token"].(string) 271 | if !ok { 272 | return "", errors.New("token not found in XBL response") 273 | } 274 | return XBLtoken, nil 275 | } 276 | 277 | // XSTSauth Holds XSTS token and UHS 278 | type XSTSauth struct { 279 | Token string 280 | UHS string 281 | } 282 | 283 | // AuthXSTS Gets XSTS token using XBL 284 | func AuthXSTS(XBLtoken string) (XSTSauth, error) { 285 | var auth XSTSauth 286 | XSTSdataMap := map[string]interface{}{ 287 | "Properties": map[string]interface{}{ 288 | "SandboxId": "RETAIL", 289 | "UserTokens": []string{XBLtoken}, 290 | }, 291 | "RelyingParty": "rp://api.minecraftservices.com/", 292 | "TokenType": "JWT", 293 | } 294 | XSTSdata, err := json.Marshal(XSTSdataMap) 295 | if err != nil { 296 | return auth, err 297 | } 298 | XSTSreq, err := http.NewRequest(http.MethodPost, "https://xsts.auth.xboxlive.com/xsts/authorize", bytes.NewBuffer(XSTSdata)) 299 | if err != nil { 300 | return auth, err 301 | } 302 | XSTSreq.Header.Set("Content-Type", "application/json") 303 | XSTSreq.Header.Set("Accept", "application/json") 304 | client := &http.Client{ 305 | Timeout: 5 * time.Second, 306 | } 307 | XSTSresp, err := client.Do(XSTSreq) 308 | if err != nil { 309 | return auth, err 310 | } 311 | var XSTSres map[string]interface{} 312 | json.NewDecoder(XSTSresp.Body).Decode(&XSTSres) 313 | XSTSresp.Body.Close() 314 | if XSTSresp.StatusCode != 200 { 315 | return auth, fmt.Errorf("XSTS answered not HTTP200! Instead got %s and following json: %#v", XSTSresp.Status, XSTSres) 316 | } 317 | XSTStoken, ok := XSTSres["Token"].(string) 318 | if !ok { 319 | return auth, errors.New("could not find Token in XSTS response") 320 | } 321 | auth.Token = XSTStoken 322 | XSTSdc, ok := XSTSres["DisplayClaims"].(map[string]interface{}) 323 | if !ok { 324 | return auth, errors.New("could not find DisplayClaims object in XSTS response") 325 | } 326 | XSTSxui, ok := XSTSdc["xui"].([]interface{}) 327 | if !ok { 328 | return auth, errors.New("could not find xui array in DisplayClaims object") 329 | } 330 | if len(XSTSxui) < 1 { 331 | return auth, errors.New("xui array in DisplayClaims object does not have any elements") 332 | } 333 | XSTSuhsObject, ok := XSTSxui[0].(map[string]interface{}) 334 | if !ok { 335 | return auth, errors.New("could not get ush object in xui array") 336 | } 337 | XSTSuhs, ok := XSTSuhsObject["uhs"].(string) 338 | if !ok { 339 | return auth, errors.New("could not get uhs string from ush object") 340 | } 341 | auth.UHS = XSTSuhs 342 | return auth, nil 343 | } 344 | 345 | // MCauth Represents Minecraft auth response 346 | type MCauth struct { 347 | Token string 348 | ExpiresAfter int64 349 | } 350 | 351 | // AuthMC Gets Minecraft authorization from XSTS token 352 | func AuthMC(token XSTSauth) (MCauth, error) { 353 | var auth MCauth 354 | MCdataMap := map[string]interface{}{ 355 | "identityToken": "XBL3.0 x=" + token.UHS + ";" + token.Token, 356 | } 357 | MCdata, err := json.Marshal(MCdataMap) 358 | if err != nil { 359 | return auth, err 360 | } 361 | MCreq, err := http.NewRequest(http.MethodPost, "https://api.minecraftservices.com/authentication/login_with_xbox", bytes.NewBuffer(MCdata)) 362 | if err != nil { 363 | return auth, err 364 | } 365 | MCreq.Header.Set("Content-Type", "application/json") 366 | MCreq.Header.Set("Accept", "application/json") 367 | client := &http.Client{ 368 | Timeout: 5 * time.Second, 369 | } 370 | MCresp, err := client.Do(MCreq) 371 | if err != nil { 372 | return auth, err 373 | } 374 | var MCres map[string]interface{} 375 | json.NewDecoder(MCresp.Body).Decode(&MCres) 376 | MCresp.Body.Close() 377 | if MCresp.StatusCode != 200 { 378 | return auth, fmt.Errorf("MC answered not HTTP200! Instead got %s and following json: %#v", MCresp.Status, MCres) 379 | } 380 | MCtoken, ok := MCres["access_token"].(string) 381 | if !ok { 382 | return auth, errors.New("could not find access_token in MC response") 383 | } 384 | auth.Token = MCtoken 385 | MCexpire, ok := MCres["expires_in"].(float64) 386 | if !ok { 387 | return auth, errors.New("could not find expires_in in MC response") 388 | } 389 | auth.ExpiresAfter = time.Now().Unix() + int64(MCexpire) 390 | return auth, nil 391 | } 392 | 393 | // GetMCprofile Gets BotAuth from token 394 | func GetMCprofile(token string) (BotAuth, error) { 395 | var profile BotAuth 396 | PRreq, err := http.NewRequest("GET", "https://api.minecraftservices.com/minecraft/profile", nil) 397 | if err != nil { 398 | return profile, err 399 | } 400 | PRreq.Header.Set("Authorization", "Bearer "+token) 401 | client := &http.Client{ 402 | Timeout: 5 * time.Second, 403 | } 404 | PRresp, err := client.Do(PRreq) 405 | if err != nil { 406 | return profile, err 407 | } 408 | var PRres map[string]interface{} 409 | json.NewDecoder(PRresp.Body).Decode(&PRres) 410 | PRresp.Body.Close() 411 | if PRresp.StatusCode != 200 { 412 | return profile, fmt.Errorf("MC (profile) answered not HTTP200! Instead got %s and following json: %#v", PRresp.Status, PRres) 413 | } 414 | PRuuid, ok := PRres["id"].(string) 415 | if !ok { 416 | return profile, errors.New("could not find uuid in profile response") 417 | } 418 | profile.UUID = PRuuid 419 | PRname, ok := PRres["name"].(string) 420 | if !ok { 421 | return profile, errors.New("could not find username in profile response") 422 | } 423 | profile.Name = PRname 424 | return profile, nil 425 | } 426 | 427 | // DefaultCacheFilename Used to load and save Microsoft auth because it gives token that lasts from a day to a week 428 | const DefaultCacheFilename = "./auth.cache" 429 | 430 | // GetMCcredentials From 0 to Minecraft BotAuth with cache using device code flow 431 | func GetMCcredentials(CacheFilename, cid string) (BotAuth, error) { 432 | var resauth BotAuth 433 | var MSa MSauth 434 | if CacheFilename == "" { 435 | CacheFilename = DefaultCacheFilename 436 | } 437 | if _, err := os.Stat(CacheFilename); os.IsNotExist(err) { 438 | var err error 439 | MSa, err = AuthMSdevice(cid) 440 | if err != nil { 441 | return resauth, err 442 | } 443 | tocache, err := json.Marshal(MSa) 444 | if err != nil { 445 | return resauth, err 446 | } 447 | err = os.WriteFile(CacheFilename, tocache, 0600) 448 | if err != nil { 449 | return resauth, err 450 | } 451 | log.Println("Got an authorization token, trying to authenticate XBL...") 452 | } else { 453 | cachecontent, err := os.ReadFile(CacheFilename) 454 | if err != nil { 455 | return resauth, err 456 | } 457 | err = json.Unmarshal(cachecontent, &MSa) 458 | if err != nil { 459 | return resauth, err 460 | } 461 | MSaOld := MSa 462 | err = CheckRefreshMS(&MSa, cid) 463 | if err != nil { 464 | return resauth, err 465 | } 466 | if MSaOld.AccessToken != MSa.AccessToken { 467 | tocache, err := json.Marshal(MSa) 468 | if err != nil { 469 | return resauth, err 470 | } 471 | err = os.WriteFile(CacheFilename, tocache, 0600) 472 | if err != nil { 473 | return resauth, err 474 | } 475 | } 476 | log.Println("Got cached authorization token, trying to authenticate XBL...") 477 | } 478 | 479 | XBLa, err := AuthXBL(MSa.AccessToken) 480 | if err != nil { 481 | return resauth, err 482 | } 483 | log.Println("Authorized on XBL, trying to get XSTS token...") 484 | 485 | XSTSa, err := AuthXSTS(XBLa) 486 | if err != nil { 487 | return resauth, err 488 | } 489 | log.Println("Got XSTS token, trying to get MC token...") 490 | 491 | MCa, err := AuthMC(XSTSa) 492 | if err != nil { 493 | return resauth, err 494 | } 495 | log.Println("Got MC token, NOT checking that you own the game because it is too complicated and going straight for MC profile...") 496 | 497 | resauth, err = GetMCprofile(MCa.Token) 498 | if err != nil { 499 | return resauth, err 500 | } 501 | log.Println("Got MC profile") 502 | log.Println("UUID: " + resauth.UUID) 503 | log.Println("Name: " + resauth.Name) 504 | resauth.AsTk = MCa.Token 505 | return resauth, nil 506 | } 507 | --------------------------------------------------------------------------------