minecraft-server-invites/auth/auth.go
Gabriel Simmer 709353b46e Store oauth session for redirect, invite un-authed
Also added Dockerfile :)
2022-07-06 00:17:41 +01:00

231 lines
5.9 KiB
Go

package auth
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"golang.org/x/oauth2"
"golang.org/x/oauth2/endpoints"
"io/ioutil"
"net/http"
"os"
"time"
"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
}