2022-07-05 11:10:18 +01:00
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"time"
|
2022-07-16 02:44:32 +01:00
|
|
|
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
"golang.org/x/oauth2/endpoints"
|
|
|
|
|
2022-07-05 11:10:18 +01:00
|
|
|
"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"`
|
|
|
|
}
|
|
|
|
|
2022-07-06 00:17:41 +01:00
|
|
|
func LoginUrl(stateId string) string {
|
2022-07-05 11:10:18 +01:00
|
|
|
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"},
|
|
|
|
}
|
2022-07-06 00:17:41 +01:00
|
|
|
return oaConfig.AuthCodeURL(stateId)
|
2022-07-05 11:10:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|