package auth import ( "bytes" "context" "crypto/tls" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "os" "time" "golang.org/x/oauth2" "golang.org/x/oauth2/endpoints" "whitelistmanager/store" ) const NoMinecraftOwnership = "minecraft is not owned" type MinecraftLoginResponse struct { Username string `json:"username"` Roles []string `json:"roles"` AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` } type MinecraftProfile struct { ID string `json:"id"` Name string `json:"name"` Error string `json:"error"` } type XblResponse struct { IssueInstant time.Time `json:"IssueInstant"` NotAfter time.Time `json:"NotAfter"` Token string `json:"Token"` DisplayClaims DisplayClaims `json:"DisplayClaims"` } type Xui struct { Uhs string `json:"uhs"` } type DisplayClaims struct { Xui []Xui `json:"xui"` } type MinecraftAuth struct { IdentityToken string `json:"identityToken"` } func LoginUrl(stateId string) string { oaConfig := &oauth2.Config{ ClientID: os.Getenv("AZURE_OAUTH_CLIENT_ID"), ClientSecret: os.Getenv("AZURE_OAUTH_CLIENT_SECRET"), Endpoint: endpoints.Microsoft, Scopes: []string{"Xboxlive.signin", "Xboxlive.offline_access"}, } return oaConfig.AuthCodeURL(stateId) } func Authenticate(code string) (store.User, error) { oaConfig := &oauth2.Config{ ClientID: os.Getenv("AZURE_OAUTH_CLIENT_ID"), ClientSecret: os.Getenv("AZURE_OAUTH_CLIENT_SECRET"), Endpoint: endpoints.Microsoft, Scopes: []string{"Xboxlive.signin", "Xboxlive.offline_access"}, } token, _ := oaConfig.Exchange(context.Background(), code) client := oaConfig.Client(context.Background(), token) client.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ Renegotiation: tls.RenegotiateOnceAsClient, InsecureSkipVerify: true, }, } minecraftToken, userProfile, err := minecraftTokenExchange(token, client) if err != nil { return store.User{}, err } user := store.User{ Id: userProfile.ID, Token: minecraftToken, DisplayName: userProfile.Name, RefreshToken: token.RefreshToken, TokenExpiry: token.Expiry, } return user, nil } func xblTokenExchange(token *oauth2.Token, client *http.Client) (string, string, error) { xblPayload := map[string]interface{}{ "RelyingParty": "http://auth.xboxlive.com", "TokenType": "JWT", "Properties": map[string]interface{}{ "AuthMethod": "RPS", "SiteName": "user.auth.xboxlive.com", "RpsTicket": "d=" + token.AccessToken, }, } encoded, err := json.Marshal(xblPayload) if err != nil { return "", "", err } request, err := http.NewRequest("POST", "https://user.auth.xboxlive.com/user/authenticate", bytes.NewReader(encoded)) if err != nil { return "", "", err } request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("x-xbl-contract-version", "1") resp, err := client.Do(request) if err != nil { return "", "", err } b, err := ioutil.ReadAll(resp.Body) if err != nil { return "", "", err } var xblData XblResponse err = json.Unmarshal(b, &xblData) if err != nil { return "", "", err } return xblData.Token, xblData.DisplayClaims.Xui[0].Uhs, nil } func xstsTokenExchange(xblToken string, client *http.Client) (string, error) { body := map[string]interface{}{ "Properties": map[string]interface{}{ "SandboxId": "RETAIL", "UserTokens": []string{ xblToken, }, }, "RelyingParty": "rp://api.minecraftservices.com/", "TokenType": "JWT", } encoded, err := json.Marshal(body) if err != nil { return "", err } request, err := http.NewRequest("POST", "https://xsts.auth.xboxlive.com/xsts/authorize", bytes.NewReader(encoded)) if err != nil { return "", err } request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("x-xbl-contract-version", "1") resp, err := client.Do(request) if err != nil { return "", err } b, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } var jsonResponse map[string]interface{} err = json.Unmarshal(b, &jsonResponse) if err != nil { return "", err } return jsonResponse["Token"].(string), nil } // Exchanges our Microsoft OAuth token for Xbox Live -> XSTS -> Mojang func minecraftTokenExchange(token *oauth2.Token, client *http.Client) (string, MinecraftProfile, error) { xblToken, userHash, err := xblTokenExchange(token, client) if err != nil { return "", MinecraftProfile{}, err } xstsToken, err := xstsTokenExchange(xblToken, client) if err != nil { return "", MinecraftProfile{}, err } body := MinecraftAuth{ IdentityToken: fmt.Sprintf("XBL3.0 x=%s;%s", userHash, xstsToken), } encoded, err := json.Marshal(body) if err != nil { return "", MinecraftProfile{}, err } request, err := http.NewRequest("POST", "https://api.minecraftservices.com/authentication/login_with_xbox", bytes.NewReader(encoded)) if err != nil { return "", MinecraftProfile{}, err } response, err := client.Do(request) if err != nil { return "", MinecraftProfile{}, err } var mcr MinecraftLoginResponse err = json.NewDecoder(response.Body).Decode(&mcr) if err != nil { return "", MinecraftProfile{}, err } verificationReq, err := http.NewRequest("GET", "https://api.minecraftservices.com/minecraft/profile", nil) if err != nil { return "", MinecraftProfile{}, err } verificationReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", mcr.AccessToken)) resp, err := client.Do(verificationReq) if err != nil { return "", MinecraftProfile{}, err } var mcp MinecraftProfile err = json.NewDecoder(resp.Body).Decode(&mcp) if err != nil { return "", MinecraftProfile{}, err } if mcp.Error != "" && mcp.ID == "" { // User does not own the game, or no ID comes back return "", MinecraftProfile{}, errors.New(NoMinecraftOwnership) } return mcr.AccessToken, mcp, nil }