2021-07-22 19:15:20 +01:00
# include <QNetworkAccessManager>
# include <QNetworkRequest>
# include <QNetworkReply>
# include <QDesktopServices>
# include <QMetaEnum>
# include <QDebug>
# include <QJsonDocument>
# include <QJsonObject>
# include <QJsonArray>
# include <QUrlQuery>
# include <QPixmap>
# include <QPainter>
2021-07-26 20:44:11 +01:00
# include "AuthContext.h"
2021-07-22 19:15:20 +01:00
# include "katabasis/Globals.h"
2021-08-30 23:55:56 +01:00
# include "AuthRequest.h"
2021-08-27 21:35:17 +01:00
# include "Secrets.h"
2021-07-22 19:15:20 +01:00
2021-08-30 23:55:56 +01:00
# include "Env.h"
2021-07-22 19:15:20 +01:00
using OAuth2 = Katabasis : : OAuth2 ;
using Activity = Katabasis : : Activity ;
2021-07-26 20:44:11 +01:00
AuthContext : : AuthContext ( AccountData * data , QObject * parent ) :
AccountTask ( data , parent )
2021-07-22 19:15:20 +01:00
{
}
2021-07-26 20:44:11 +01:00
void AuthContext : : beginActivity ( Activity activity ) {
2021-07-22 19:15:20 +01:00
if ( isBusy ( ) ) {
throw 0 ;
}
2021-07-26 20:44:11 +01:00
m_activity = activity ;
changeState ( STATE_WORKING , " Initializing " ) ;
emit activityChanged ( m_activity ) ;
2021-07-22 19:15:20 +01:00
}
2021-07-26 20:44:11 +01:00
void AuthContext : : finishActivity ( ) {
2021-07-22 19:15:20 +01:00
if ( ! isBusy ( ) ) {
throw 0 ;
}
2021-07-26 20:44:11 +01:00
m_activity = Katabasis : : Activity : : Idle ;
2021-08-22 19:01:18 +01:00
setStage ( AuthStage : : Complete ) ;
2021-07-26 20:44:11 +01:00
m_data - > validity_ = m_data - > minecraftProfile . validity ;
emit activityChanged ( m_activity ) ;
2021-07-22 19:15:20 +01:00
}
2021-07-26 20:44:11 +01:00
void AuthContext : : initMSA ( ) {
if ( m_oauth2 ) {
return ;
}
2021-09-05 17:23:49 +01:00
auto clientId = Secrets : : getMSAClientID ( ' - ' ) ;
if ( clientId . isEmpty ( ) ) {
return ;
}
2021-07-26 20:44:11 +01:00
Katabasis : : OAuth2 : : Options opts ;
opts . scope = " XboxLive.signin offline_access " ;
2021-09-05 17:23:49 +01:00
opts . clientIdentifier = clientId ;
2021-08-22 19:01:18 +01:00
opts . authorizationUrl = " https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode " ;
opts . accessTokenUrl = " https://login.microsoftonline.com/consumers/oauth2/v2.0/token " ;
2021-07-26 20:44:11 +01:00
opts . listenerPorts = { 28562 , 28563 , 28564 , 28565 , 28566 } ;
2021-07-22 19:15:20 +01:00
2021-08-30 23:55:56 +01:00
m_oauth2 = new OAuth2 ( opts , m_data - > msaToken , this , & ENV . qnam ( ) ) ;
2021-08-22 19:01:18 +01:00
m_oauth2 - > setGrantFlow ( Katabasis : : OAuth2 : : GrantFlowDevice ) ;
2021-07-22 19:15:20 +01:00
2021-07-26 20:44:11 +01:00
connect ( m_oauth2 , & OAuth2 : : linkingFailed , this , & AuthContext : : onOAuthLinkingFailed ) ;
connect ( m_oauth2 , & OAuth2 : : linkingSucceeded , this , & AuthContext : : onOAuthLinkingSucceeded ) ;
2021-08-22 19:01:18 +01:00
connect ( m_oauth2 , & OAuth2 : : showVerificationUriAndCode , this , & AuthContext : : showVerificationUriAndCode ) ;
2021-07-26 20:44:11 +01:00
connect ( m_oauth2 , & OAuth2 : : activityChanged , this , & AuthContext : : onOAuthActivityChanged ) ;
2021-07-22 19:15:20 +01:00
}
2021-07-26 20:44:11 +01:00
void AuthContext : : initMojang ( ) {
if ( m_yggdrasil ) {
return ;
2021-07-22 19:15:20 +01:00
}
2021-07-26 20:44:11 +01:00
m_yggdrasil = new Yggdrasil ( m_data , this ) ;
2021-07-22 19:15:20 +01:00
2021-07-26 20:44:11 +01:00
connect ( m_yggdrasil , & Task : : failed , this , & AuthContext : : onMojangFailed ) ;
connect ( m_yggdrasil , & Task : : succeeded , this , & AuthContext : : onMojangSucceeded ) ;
2021-07-22 19:15:20 +01:00
}
2021-07-26 20:44:11 +01:00
void AuthContext : : onMojangSucceeded ( ) {
doMinecraftProfile ( ) ;
}
2021-07-22 19:15:20 +01:00
2021-07-26 20:44:11 +01:00
void AuthContext : : onMojangFailed ( ) {
finishActivity ( ) ;
m_error = m_yggdrasil - > m_error ;
m_aborted = m_yggdrasil - > m_aborted ;
2021-08-15 22:40:37 +01:00
changeState ( m_yggdrasil - > accountState ( ) , tr ( " Mojang user authentication failed. " ) ) ;
2021-07-22 19:15:20 +01:00
}
2021-07-26 20:44:11 +01:00
/*
bool AuthContext : : signOut ( ) {
2021-07-22 19:15:20 +01:00
if ( isBusy ( ) ) {
return false ;
}
2021-07-26 20:44:11 +01:00
start ( ) ;
2021-07-22 19:15:20 +01:00
beginActivity ( Activity : : LoggingOut ) ;
2021-07-26 20:44:11 +01:00
m_oauth2 - > unlink ( ) ;
2021-07-22 19:15:20 +01:00
m_account = AccountData ( ) ;
finishActivity ( ) ;
return true ;
}
2021-07-26 20:44:11 +01:00
*/
2021-07-22 19:15:20 +01:00
2021-07-26 20:44:11 +01:00
void AuthContext : : onOAuthLinkingFailed ( ) {
2021-08-22 19:01:18 +01:00
emit hideVerificationUriAndCode ( ) ;
2021-07-22 19:15:20 +01:00
finishActivity ( ) ;
2021-08-15 22:40:37 +01:00
changeState ( STATE_FAILED_HARD , tr ( " Microsoft user authentication failed. " ) ) ;
2021-07-22 19:15:20 +01:00
}
2021-07-26 20:44:11 +01:00
void AuthContext : : onOAuthLinkingSucceeded ( ) {
2021-08-22 19:01:18 +01:00
emit hideVerificationUriAndCode ( ) ;
2021-07-22 19:15:20 +01:00
auto * o2t = qobject_cast < OAuth2 * > ( sender ( ) ) ;
if ( ! o2t - > linked ( ) ) {
finishActivity ( ) ;
2021-08-15 22:40:37 +01:00
changeState ( STATE_FAILED_HARD , tr ( " Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time). " ) ) ;
2021-07-22 19:15:20 +01:00
return ;
}
QVariantMap extraTokens = o2t - > extraTokens ( ) ;
2021-08-18 23:43:19 +01:00
# ifndef NDEBUG
2021-07-22 19:15:20 +01:00
if ( ! extraTokens . isEmpty ( ) ) {
qDebug ( ) < < " Extra tokens in response: " ;
foreach ( QString key , extraTokens . keys ( ) ) {
qDebug ( ) < < " \t " < < key < < " : " < < extraTokens . value ( key ) ;
}
}
2021-08-18 23:43:19 +01:00
# endif
2021-07-22 19:15:20 +01:00
doUserAuth ( ) ;
}
2021-07-26 20:44:11 +01:00
void AuthContext : : onOAuthActivityChanged ( Katabasis : : Activity activity ) {
2021-07-22 19:15:20 +01:00
// respond to activity change here
}
2021-07-26 20:44:11 +01:00
void AuthContext : : doUserAuth ( ) {
2021-08-22 19:01:18 +01:00
setStage ( AuthStage : : UserAuth ) ;
2021-08-15 22:40:37 +01:00
changeState ( STATE_WORKING , tr ( " Starting user authentication " ) ) ;
2021-07-26 20:44:11 +01:00
2021-07-22 19:15:20 +01:00
QString xbox_auth_template = R " XXX(
{
" Properties " : {
" AuthMethod " : " RPS " ,
" SiteName " : " user.auth.xboxlive.com " ,
" RpsTicket " : " d=%1 "
} ,
" RelyingParty " : " http://auth.xboxlive.com " ,
" TokenType " : " JWT "
}
) XXX " ;
2021-07-26 20:44:11 +01:00
auto xbox_auth_data = xbox_auth_template . arg ( m_data - > msaToken . token ) ;
2021-07-22 19:15:20 +01:00
QNetworkRequest request = QNetworkRequest ( QUrl ( " https://user.auth.xboxlive.com/user/authenticate " ) ) ;
request . setHeader ( QNetworkRequest : : ContentTypeHeader , " application/json " ) ;
request . setRawHeader ( " Accept " , " application/json " ) ;
2021-09-21 21:02:12 +01:00
auto * requestor = new AuthRequest ( this ) ;
connect ( requestor , & AuthRequest : : finished , this , & AuthContext : : onUserAuthDone ) ;
2021-07-22 19:15:20 +01:00
requestor - > post ( request , xbox_auth_data . toUtf8 ( ) ) ;
qDebug ( ) < < " First layer of XBox auth ... commencing. " ;
}
namespace {
bool getDateTime ( QJsonValue value , QDateTime & out ) {
if ( ! value . isString ( ) ) {
return false ;
}
2021-07-26 20:44:11 +01:00
out = QDateTime : : fromString ( value . toString ( ) , Qt : : ISODate ) ;
2021-07-22 19:15:20 +01:00
return out . isValid ( ) ;
}
bool getString ( QJsonValue value , QString & out ) {
if ( ! value . isString ( ) ) {
return false ;
}
out = value . toString ( ) ;
return true ;
}
bool getNumber ( QJsonValue value , double & out ) {
if ( ! value . isDouble ( ) ) {
return false ;
}
out = value . toDouble ( ) ;
return true ;
}
2021-08-30 23:55:56 +01:00
bool getNumber ( QJsonValue value , int64_t & out ) {
if ( ! value . isDouble ( ) ) {
return false ;
}
out = ( int64_t ) value . toDouble ( ) ;
return true ;
}
2021-08-29 21:55:33 +01:00
bool getBool ( QJsonValue value , bool & out ) {
if ( ! value . isBool ( ) ) {
return false ;
}
out = value . toBool ( ) ;
return true ;
}
2021-07-22 19:15:20 +01:00
/*
{
" IssueInstant " : " 2020-12-07T19:52:08.4463796Z " ,
" NotAfter " : " 2020-12-21T19:52:08.4463796Z " ,
" Token " : " token " ,
" DisplayClaims " : {
" xui " : [
{
" uhs " : " userhash "
}
]
}
}
*/
// TODO: handle error responses ...
/*
{
" Identity " : " 0 " ,
" XErr " : 2148916238 ,
" Message " : " " ,
" Redirect " : " https://start.ui.xboxlive.com/AddChildToFamily "
}
// 2148916233 = missing XBox account
// 2148916238 = child account not linked to a family
*/
2021-08-18 23:43:19 +01:00
bool parseXTokenResponse ( QByteArray & data , Katabasis : : Token & output , const char * name ) {
2021-08-19 09:27:22 +01:00
qDebug ( ) < < " Parsing " < < name < < " : " ;
2021-08-18 23:43:19 +01:00
# ifndef NDEBUG
qDebug ( ) < < data ;
# endif
2021-07-22 19:15:20 +01:00
QJsonParseError jsonError ;
QJsonDocument doc = QJsonDocument : : fromJson ( data , & jsonError ) ;
if ( jsonError . error ) {
qWarning ( ) < < " Failed to parse response from user.auth.xboxlive.com as JSON: " < < jsonError . errorString ( ) ;
return false ;
}
auto obj = doc . object ( ) ;
if ( ! getDateTime ( obj . value ( " IssueInstant " ) , output . issueInstant ) ) {
qWarning ( ) < < " User IssueInstant is not a timestamp " ;
return false ;
}
if ( ! getDateTime ( obj . value ( " NotAfter " ) , output . notAfter ) ) {
qWarning ( ) < < " User NotAfter is not a timestamp " ;
return false ;
}
if ( ! getString ( obj . value ( " Token " ) , output . token ) ) {
qWarning ( ) < < " User Token is not a timestamp " ;
return false ;
}
auto arrayVal = obj . value ( " DisplayClaims " ) . toObject ( ) . value ( " xui " ) ;
if ( ! arrayVal . isArray ( ) ) {
qWarning ( ) < < " Missing xui claims array " ;
return false ;
}
bool foundUHS = false ;
for ( auto item : arrayVal . toArray ( ) ) {
if ( ! item . isObject ( ) ) {
continue ;
}
auto obj = item . toObject ( ) ;
if ( obj . contains ( " uhs " ) ) {
foundUHS = true ;
} else {
continue ;
}
// consume all 'display claims' ... whatever that means
for ( auto iter = obj . begin ( ) ; iter ! = obj . end ( ) ; iter + + ) {
QString claim ;
if ( ! getString ( obj . value ( iter . key ( ) ) , claim ) ) {
qWarning ( ) < < " display claim " < < iter . key ( ) < < " is not a string... " ;
return false ;
}
output . extra [ iter . key ( ) ] = claim ;
}
break ;
}
if ( ! foundUHS ) {
qWarning ( ) < < " Missing uhs " ;
return false ;
}
output . validity = Katabasis : : Validity : : Certain ;
2021-08-19 09:27:22 +01:00
qDebug ( ) < < name < < " is valid. " ;
2021-07-22 19:15:20 +01:00
return true ;
}
}
2021-07-26 20:44:11 +01:00
void AuthContext : : onUserAuthDone (
2021-07-22 19:15:20 +01:00
QNetworkReply : : NetworkError error ,
QByteArray replyData ,
QList < QNetworkReply : : RawHeaderPair > headers
) {
if ( error ! = QNetworkReply : : NoError ) {
qWarning ( ) < < " Reply error: " < < error ;
finishActivity ( ) ;
2021-08-15 22:40:37 +01:00
changeState ( STATE_FAILED_HARD , tr ( " XBox user authentication failed. " ) ) ;
2021-07-22 19:15:20 +01:00
return ;
}
Katabasis : : Token temp ;
2021-08-18 23:43:19 +01:00
if ( ! parseXTokenResponse ( replyData , temp , " UToken " ) ) {
2021-07-22 19:15:20 +01:00
qWarning ( ) < < " Could not parse user authentication response... " ;
finishActivity ( ) ;
2021-08-15 22:40:37 +01:00
changeState ( STATE_FAILED_HARD , tr ( " XBox user authentication response could not be understood. " ) ) ;
2021-07-22 19:15:20 +01:00
return ;
}
2021-07-26 20:44:11 +01:00
m_data - > userToken = temp ;
2021-08-22 19:01:18 +01:00
setStage ( AuthStage : : XboxAuth ) ;
2021-08-15 22:40:37 +01:00
changeState ( STATE_WORKING , tr ( " Starting XBox authentication " ) ) ;
2021-07-22 19:15:20 +01:00
doSTSAuthMinecraft ( ) ;
doSTSAuthGeneric ( ) ;
}
/*
url = " https://xsts.auth.xboxlive.com/xsts/authorize "
headers = { " x-xbl-contract-version " : " 1 " }
data = {
" RelyingParty " : relying_party ,
" TokenType " : " JWT " ,
" Properties " : {
" UserTokens " : [ self . user_token . token ] ,
" SandboxId " : " RETAIL " ,
} ,
}
*/
2021-07-26 20:44:11 +01:00
void AuthContext : : doSTSAuthMinecraft ( ) {
2021-07-22 19:15:20 +01:00
QString xbox_auth_template = R " XXX(
{
" Properties " : {
" SandboxId " : " RETAIL " ,
" UserTokens " : [
" %1 "
]
} ,
" RelyingParty " : " rp://api.minecraftservices.com/ " ,
" TokenType " : " JWT "
}
) XXX " ;
2021-07-26 20:44:11 +01:00
auto xbox_auth_data = xbox_auth_template . arg ( m_data - > userToken . token ) ;
2021-07-22 19:15:20 +01:00
QNetworkRequest request = QNetworkRequest ( QUrl ( " https://xsts.auth.xboxlive.com/xsts/authorize " ) ) ;
request . setHeader ( QNetworkRequest : : ContentTypeHeader , " application/json " ) ;
request . setRawHeader ( " Accept " , " application/json " ) ;
2021-09-21 21:02:12 +01:00
AuthRequest * requestor = new AuthRequest ( this ) ;
connect ( requestor , & AuthRequest : : finished , this , & AuthContext : : onSTSAuthMinecraftDone ) ;
2021-07-22 19:15:20 +01:00
requestor - > post ( request , xbox_auth_data . toUtf8 ( ) ) ;
2021-08-18 23:43:19 +01:00
qDebug ( ) < < " Getting Minecraft services STS token... " ;
2021-07-22 19:15:20 +01:00
}
2021-08-30 23:55:56 +01:00
void AuthContext : : processSTSError ( QNetworkReply : : NetworkError error , QByteArray data , QList < QNetworkReply : : RawHeaderPair > headers ) {
if ( error = = QNetworkReply : : AuthenticationRequiredError ) {
QJsonParseError jsonError ;
QJsonDocument doc = QJsonDocument : : fromJson ( data , & jsonError ) ;
if ( jsonError . error ) {
qWarning ( ) < < " Cannot parse error XSTS response as JSON: " < < jsonError . errorString ( ) ;
return ;
}
2021-07-22 19:15:20 +01:00
2021-08-30 23:55:56 +01:00
int64_t errorCode = - 1 ;
auto obj = doc . object ( ) ;
if ( ! getNumber ( obj . value ( " XErr " ) , errorCode ) ) {
qWarning ( ) < < " XErr is not a number " ;
return ;
}
stsErrors . insert ( errorCode ) ;
stsFailed = true ;
2021-07-22 19:15:20 +01:00
}
}
2021-08-30 23:55:56 +01:00
void AuthContext : : onSTSAuthMinecraftDone (
2021-07-22 19:15:20 +01:00
QNetworkReply : : NetworkError error ,
QByteArray replyData ,
QList < QNetworkReply : : RawHeaderPair > headers
) {
2021-08-30 23:55:56 +01:00
# ifndef NDEBUG
qDebug ( ) < < replyData ;
# endif
2021-07-22 19:15:20 +01:00
if ( error ! = QNetworkReply : : NoError ) {
qWarning ( ) < < " Reply error: " < < error ;
2021-08-30 23:55:56 +01:00
processSTSError ( error , replyData , headers ) ;
failResult ( m_mcAuthSucceeded ) ;
2021-07-22 19:15:20 +01:00
return ;
}
Katabasis : : Token temp ;
2021-08-30 23:55:56 +01:00
if ( ! parseXTokenResponse ( replyData , temp , " STSAuthMinecraft " ) ) {
qWarning ( ) < < " Could not parse authorization response for access to mojang services... " ;
failResult ( m_mcAuthSucceeded ) ;
2021-07-22 19:15:20 +01:00
return ;
}
2021-07-26 20:44:11 +01:00
if ( temp . extra [ " uhs " ] ! = m_data - > userToken . extra [ " uhs " ] ) {
2021-07-22 19:15:20 +01:00
qWarning ( ) < < " Server has changed user hash in the reply... something is wrong. ABORTING " ;
2021-08-30 23:55:56 +01:00
failResult ( m_mcAuthSucceeded ) ;
2021-07-22 19:15:20 +01:00
return ;
}
2021-08-30 23:55:56 +01:00
m_data - > mojangservicesToken = temp ;
2021-07-22 19:15:20 +01:00
2021-08-30 23:55:56 +01:00
doMinecraftAuth ( ) ;
2021-07-22 19:15:20 +01:00
}
2021-07-26 20:44:11 +01:00
void AuthContext : : doMinecraftAuth ( ) {
2021-07-22 19:15:20 +01:00
QString mc_auth_template = R " XXX(
{
" identityToken " : " XBL3.0 x=%1;%2 "
}
) XXX " ;
2021-07-26 20:44:11 +01:00
auto data = mc_auth_template . arg ( m_data - > mojangservicesToken . extra [ " uhs " ] . toString ( ) , m_data - > mojangservicesToken . token ) ;
2021-07-22 19:15:20 +01:00
QNetworkRequest request = QNetworkRequest ( QUrl ( " https://api.minecraftservices.com/authentication/login_with_xbox " ) ) ;
request . setHeader ( QNetworkRequest : : ContentTypeHeader , " application/json " ) ;
request . setRawHeader ( " Accept " , " application/json " ) ;
2021-09-21 21:02:12 +01:00
AuthRequest * requestor = new AuthRequest ( this ) ;
connect ( requestor , & AuthRequest : : finished , this , & AuthContext : : onMinecraftAuthDone ) ;
2021-07-22 19:15:20 +01:00
requestor - > post ( request , data . toUtf8 ( ) ) ;
qDebug ( ) < < " Getting Minecraft access token... " ;
}
namespace {
bool parseMojangResponse ( QByteArray & data , Katabasis : : Token & output ) {
QJsonParseError jsonError ;
2021-08-18 23:43:19 +01:00
qDebug ( ) < < " Parsing Mojang response... " ;
# ifndef NDEBUG
qDebug ( ) < < data ;
# endif
2021-07-22 19:15:20 +01:00
QJsonDocument doc = QJsonDocument : : fromJson ( data , & jsonError ) ;
if ( jsonError . error ) {
2021-08-18 23:43:19 +01:00
qWarning ( ) < < " Failed to parse response from api.minecraftservices.com/authentication/login_with_xbox as JSON: " < < jsonError . errorString ( ) ;
2021-07-22 19:15:20 +01:00
return false ;
}
auto obj = doc . object ( ) ;
double expires_in = 0 ;
if ( ! getNumber ( obj . value ( " expires_in " ) , expires_in ) ) {
qWarning ( ) < < " expires_in is not a valid number " ;
return false ;
}
auto currentTime = QDateTime : : currentDateTimeUtc ( ) ;
output . issueInstant = currentTime ;
output . notAfter = currentTime . addSecs ( expires_in ) ;
QString username ;
if ( ! getString ( obj . value ( " username " ) , username ) ) {
qWarning ( ) < < " username is not valid " ;
return false ;
}
// TODO: it's a JWT... validate it?
if ( ! getString ( obj . value ( " access_token " ) , output . token ) ) {
qWarning ( ) < < " access_token is not valid " ;
return false ;
}
output . validity = Katabasis : : Validity : : Certain ;
2021-08-18 23:43:19 +01:00
qDebug ( ) < < " Mojang response is valid. " ;
2021-07-22 19:15:20 +01:00
return true ;
}
}
2021-07-26 20:44:11 +01:00
void AuthContext : : onMinecraftAuthDone (
2021-07-22 19:15:20 +01:00
QNetworkReply : : NetworkError error ,
QByteArray replyData ,
QList < QNetworkReply : : RawHeaderPair > headers
) {
if ( error ! = QNetworkReply : : NoError ) {
qWarning ( ) < < " Reply error: " < < error ;
2021-08-18 23:43:19 +01:00
# ifndef NDEBUG
2021-07-22 19:15:20 +01:00
qDebug ( ) < < replyData ;
2021-08-18 23:43:19 +01:00
# endif
2021-08-30 23:55:56 +01:00
failResult ( m_mcAuthSucceeded ) ;
2021-07-22 19:15:20 +01:00
return ;
}
2021-07-26 20:44:11 +01:00
if ( ! parseMojangResponse ( replyData , m_data - > yggdrasilToken ) ) {
2021-07-22 19:15:20 +01:00
qWarning ( ) < < " Could not parse login_with_xbox response... " ;
2021-08-18 23:43:19 +01:00
# ifndef NDEBUG
2021-07-22 19:15:20 +01:00
qDebug ( ) < < replyData ;
2021-08-18 23:43:19 +01:00
# endif
2021-08-30 23:55:56 +01:00
failResult ( m_mcAuthSucceeded ) ;
2021-07-22 19:15:20 +01:00
return ;
}
2021-08-30 23:55:56 +01:00
succeedResult ( m_mcAuthSucceeded ) ;
}
void AuthContext : : doSTSAuthGeneric ( ) {
QString xbox_auth_template = R " XXX(
{
" Properties " : {
" SandboxId " : " RETAIL " ,
" UserTokens " : [
" %1 "
]
} ,
" RelyingParty " : " http://xboxlive.com " ,
" TokenType " : " JWT "
}
) XXX " ;
auto xbox_auth_data = xbox_auth_template . arg ( m_data - > userToken . token ) ;
QNetworkRequest request = QNetworkRequest ( QUrl ( " https://xsts.auth.xboxlive.com/xsts/authorize " ) ) ;
request . setHeader ( QNetworkRequest : : ContentTypeHeader , " application/json " ) ;
request . setRawHeader ( " Accept " , " application/json " ) ;
2021-09-21 21:02:12 +01:00
AuthRequest * requestor = new AuthRequest ( this ) ;
connect ( requestor , & AuthRequest : : finished , this , & AuthContext : : onSTSAuthGenericDone ) ;
2021-08-30 23:55:56 +01:00
requestor - > post ( request , xbox_auth_data . toUtf8 ( ) ) ;
qDebug ( ) < < " Getting generic STS token... " ;
}
void AuthContext : : onSTSAuthGenericDone (
QNetworkReply : : NetworkError error ,
QByteArray replyData ,
QList < QNetworkReply : : RawHeaderPair > headers
) {
# ifndef NDEBUG
qDebug ( ) < < replyData ;
# endif
if ( error ! = QNetworkReply : : NoError ) {
qWarning ( ) < < " Reply error: " < < error ;
processSTSError ( error , replyData , headers ) ;
failResult ( m_xboxProfileSucceeded ) ;
return ;
}
Katabasis : : Token temp ;
2021-09-04 20:27:09 +01:00
if ( ! parseXTokenResponse ( replyData , temp , " STSAuthGeneric " ) ) {
2021-08-30 23:55:56 +01:00
qWarning ( ) < < " Could not parse authorization response for access to xbox API... " ;
failResult ( m_xboxProfileSucceeded ) ;
return ;
}
if ( temp . extra [ " uhs " ] ! = m_data - > userToken . extra [ " uhs " ] ) {
qWarning ( ) < < " Server has changed user hash in the reply... something is wrong. ABORTING " ;
failResult ( m_xboxProfileSucceeded ) ;
return ;
}
m_data - > xboxApiToken = temp ;
doXBoxProfile ( ) ;
2021-07-22 19:15:20 +01:00
}
2021-07-26 20:44:11 +01:00
void AuthContext : : doXBoxProfile ( ) {
2021-07-22 19:15:20 +01:00
auto url = QUrl ( " https://profile.xboxlive.com/users/me/profile/settings " ) ;
QUrlQuery q ;
q . addQueryItem (
" settings " ,
" GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw, "
" PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix, "
" UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep, "
" PreferredColor,Location,Bio,Watermarks, "
" RealName,RealNameOverride,IsQuarantined "
) ;
url . setQuery ( q ) ;
QNetworkRequest request = QNetworkRequest ( url ) ;
request . setHeader ( QNetworkRequest : : ContentTypeHeader , " application/json " ) ;
request . setRawHeader ( " Accept " , " application/json " ) ;
request . setRawHeader ( " x-xbl-contract-version " , " 3 " ) ;
2021-07-26 20:44:11 +01:00
request . setRawHeader ( " Authorization " , QString ( " XBL3.0 x=%1;%2 " ) . arg ( m_data - > userToken . extra [ " uhs " ] . toString ( ) , m_data - > xboxApiToken . token ) . toUtf8 ( ) ) ;
2021-09-21 21:02:12 +01:00
AuthRequest * requestor = new AuthRequest ( this ) ;
connect ( requestor , & AuthRequest : : finished , this , & AuthContext : : onXBoxProfileDone ) ;
2021-07-22 19:15:20 +01:00
requestor - > get ( request ) ;
qDebug ( ) < < " Getting Xbox profile... " ;
}
2021-07-26 20:44:11 +01:00
void AuthContext : : onXBoxProfileDone (
2021-07-22 19:15:20 +01:00
QNetworkReply : : NetworkError error ,
QByteArray replyData ,
QList < QNetworkReply : : RawHeaderPair > headers
) {
if ( error ! = QNetworkReply : : NoError ) {
qWarning ( ) < < " Reply error: " < < error ;
2021-08-18 23:43:19 +01:00
# ifndef NDEBUG
2021-07-22 19:15:20 +01:00
qDebug ( ) < < replyData ;
2021-08-18 23:43:19 +01:00
# endif
2021-08-30 23:55:56 +01:00
failResult ( m_xboxProfileSucceeded ) ;
2021-07-22 19:15:20 +01:00
return ;
}
2021-08-18 23:43:19 +01:00
# ifndef NDEBUG
2021-07-22 19:15:20 +01:00
qDebug ( ) < < " XBox profile: " < < replyData ;
2021-08-18 23:43:19 +01:00
# endif
2021-07-22 19:15:20 +01:00
2021-08-30 23:55:56 +01:00
succeedResult ( m_xboxProfileSucceeded ) ;
}
void AuthContext : : succeedResult ( bool & flag ) {
m_requestsDone + + ;
flag = true ;
checkResult ( ) ;
}
void AuthContext : : failResult ( bool & flag ) {
m_requestsDone + + ;
flag = false ;
2021-07-22 19:15:20 +01:00
checkResult ( ) ;
}
2021-07-26 20:44:11 +01:00
void AuthContext : : checkResult ( ) {
2021-08-20 00:34:32 +01:00
qDebug ( ) < < " AuthContext::checkResult called " ;
2021-07-26 20:44:11 +01:00
if ( m_requestsDone ! = 2 ) {
2021-08-20 00:34:32 +01:00
qDebug ( ) < < " Number of ready results: " < < m_requestsDone ;
2021-07-22 19:15:20 +01:00
return ;
}
2021-07-26 20:44:11 +01:00
if ( m_mcAuthSucceeded & & m_xboxProfileSucceeded ) {
2021-07-22 19:15:20 +01:00
doMinecraftProfile ( ) ;
}
else {
finishActivity ( ) ;
2021-08-30 23:55:56 +01:00
if ( stsFailed ) {
if ( stsErrors . contains ( 2148916233 ) ) {
changeState (
STATE_FAILED_HARD ,
tr ( " This Microsoft account does not have an XBox Live profile. Buy the game on %1 first. " )
. arg ( " <a href= \" https://www.minecraft.net/en-us/store/minecraft-java-edition \" >minecraft.net</a> " )
) ;
}
else if ( stsErrors . contains ( 2148916235 ) ) {
// NOTE: this is the Grulovia error
changeState (
STATE_FAILED_HARD ,
tr ( " XBox Live is not available in your country. You've been blocked. " )
) ;
}
else if ( stsErrors . contains ( 2148916238 ) ) {
changeState (
STATE_FAILED_HARD ,
tr ( " This Microsoft account is underaged and is not linked to a family. \n \n Please set up your account according to %1. " )
. arg ( " <a href= \" https://help.minecraft.net/hc/en-us/articles/360042649591 \" >help.minecraft.net</a> " )
) ;
}
else {
QStringList errorList ;
for ( auto & error : stsErrors ) {
errorList . append ( QString : : number ( error ) ) ;
}
changeState (
STATE_FAILED_HARD ,
tr ( " XSTS authentication ended with unrecognized error(s): \n \n %1 " ) . arg ( errorList . join ( " \n " ) )
) ;
}
}
else {
changeState ( STATE_FAILED_HARD , tr ( " XBox and/or Mojang authentication steps did not succeed " ) ) ;
}
2021-07-22 19:15:20 +01:00
}
}
namespace {
bool parseMinecraftProfile ( QByteArray & data , MinecraftProfile & output ) {
2021-08-18 23:43:19 +01:00
qDebug ( ) < < " Parsing Minecraft profile... " ;
# ifndef NDEBUG
qDebug ( ) < < data ;
# endif
2021-07-22 19:15:20 +01:00
QJsonParseError jsonError ;
QJsonDocument doc = QJsonDocument : : fromJson ( data , & jsonError ) ;
if ( jsonError . error ) {
qWarning ( ) < < " Failed to parse response from user.auth.xboxlive.com as JSON: " < < jsonError . errorString ( ) ;
return false ;
}
auto obj = doc . object ( ) ;
if ( ! getString ( obj . value ( " id " ) , output . id ) ) {
2021-08-18 23:43:19 +01:00
qWarning ( ) < < " Minecraft profile id is not a string " ;
2021-07-22 19:15:20 +01:00
return false ;
}
if ( ! getString ( obj . value ( " name " ) , output . name ) ) {
2021-08-18 23:43:19 +01:00
qWarning ( ) < < " Minecraft profile name is not a string " ;
2021-07-22 19:15:20 +01:00
return false ;
}
auto skinsArray = obj . value ( " skins " ) . toArray ( ) ;
for ( auto skin : skinsArray ) {
auto skinObj = skin . toObject ( ) ;
Skin skinOut ;
if ( ! getString ( skinObj . value ( " id " ) , skinOut . id ) ) {
continue ;
}
QString state ;
if ( ! getString ( skinObj . value ( " state " ) , state ) ) {
continue ;
}
if ( state ! = " ACTIVE " ) {
continue ;
}
if ( ! getString ( skinObj . value ( " url " ) , skinOut . url ) ) {
continue ;
}
if ( ! getString ( skinObj . value ( " variant " ) , skinOut . variant ) ) {
continue ;
}
// we deal with only the active skin
output . skin = skinOut ;
break ;
}
auto capesArray = obj . value ( " capes " ) . toArray ( ) ;
2021-08-20 00:34:32 +01:00
QString currentCape ;
2021-07-22 19:15:20 +01:00
for ( auto cape : capesArray ) {
auto capeObj = cape . toObject ( ) ;
Cape capeOut ;
if ( ! getString ( capeObj . value ( " id " ) , capeOut . id ) ) {
continue ;
}
QString state ;
if ( ! getString ( capeObj . value ( " state " ) , state ) ) {
continue ;
}
if ( state = = " ACTIVE " ) {
2021-08-20 00:34:32 +01:00
currentCape = capeOut . id ;
2021-07-22 19:15:20 +01:00
}
if ( ! getString ( capeObj . value ( " url " ) , capeOut . url ) ) {
continue ;
}
if ( ! getString ( capeObj . value ( " alias " ) , capeOut . alias ) ) {
continue ;
}
2021-08-20 00:34:32 +01:00
output . capes [ capeOut . id ] = capeOut ;
2021-07-22 19:15:20 +01:00
}
output . currentCape = currentCape ;
output . validity = Katabasis : : Validity : : Certain ;
return true ;
}
}
2021-07-26 20:44:11 +01:00
void AuthContext : : doMinecraftProfile ( ) {
2021-08-22 19:01:18 +01:00
setStage ( AuthStage : : MinecraftProfile ) ;
2021-08-15 22:40:37 +01:00
changeState ( STATE_WORKING , tr ( " Starting minecraft profile acquisition " ) ) ;
2021-07-26 20:44:11 +01:00
2021-07-22 19:15:20 +01:00
auto url = QUrl ( " https://api.minecraftservices.com/minecraft/profile " ) ;
QNetworkRequest request = QNetworkRequest ( url ) ;
request . setHeader ( QNetworkRequest : : ContentTypeHeader , " application/json " ) ;
// request.setRawHeader("Accept", "application/json");
2021-07-26 20:44:11 +01:00
request . setRawHeader ( " Authorization " , QString ( " Bearer %1 " ) . arg ( m_data - > yggdrasilToken . token ) . toUtf8 ( ) ) ;
2021-07-22 19:15:20 +01:00
2021-09-21 21:02:12 +01:00
AuthRequest * requestor = new AuthRequest ( this ) ;
connect ( requestor , & AuthRequest : : finished , this , & AuthContext : : onMinecraftProfileDone ) ;
2021-07-22 19:15:20 +01:00
requestor - > get ( request ) ;
}
2021-08-30 23:55:56 +01:00
void AuthContext : : onMinecraftProfileDone (
QNetworkReply : : NetworkError error ,
QByteArray data ,
QList < QNetworkReply : : RawHeaderPair > headers
) {
# ifndef NDEBUG
2021-07-22 19:15:20 +01:00
qDebug ( ) < < data ;
2021-08-30 23:55:56 +01:00
# endif
2021-07-22 19:15:20 +01:00
if ( error = = QNetworkReply : : ContentNotFoundError ) {
2021-07-26 20:44:11 +01:00
m_data - > minecraftProfile = MinecraftProfile ( ) ;
2021-07-22 19:15:20 +01:00
finishActivity ( ) ;
2021-08-20 00:34:32 +01:00
changeState ( STATE_FAILED_HARD , tr ( " Account is missing a Minecraft Java profile. \n \n While the Microsoft account is valid, it does not own the game. \n \n You might own Bedrock on this account, but that does not give you access to Java currently. " ) ) ;
2021-07-22 19:15:20 +01:00
return ;
}
if ( error ! = QNetworkReply : : NoError ) {
finishActivity ( ) ;
2021-08-20 00:34:32 +01:00
changeState ( STATE_FAILED_HARD , tr ( " Minecraft Java profile acquisition failed. " ) ) ;
2021-07-22 19:15:20 +01:00
return ;
}
2021-07-26 20:44:11 +01:00
if ( ! parseMinecraftProfile ( data , m_data - > minecraftProfile ) ) {
m_data - > minecraftProfile = MinecraftProfile ( ) ;
2021-07-22 19:15:20 +01:00
finishActivity ( ) ;
2021-08-20 00:34:32 +01:00
changeState ( STATE_FAILED_HARD , tr ( " Minecraft Java profile response could not be parsed " ) ) ;
2021-07-22 19:15:20 +01:00
return ;
}
2021-08-29 21:55:33 +01:00
if ( m_data - > type = = AccountType : : Mojang ) {
doMigrationEligibilityCheck ( ) ;
}
else {
doGetSkin ( ) ;
}
}
void AuthContext : : doMigrationEligibilityCheck ( ) {
setStage ( AuthStage : : MigrationEligibility ) ;
changeState ( STATE_WORKING , tr ( " Starting check for migration eligibility " ) ) ;
auto url = QUrl ( " https://api.minecraftservices.com/rollout/v1/msamigration " ) ;
QNetworkRequest request = QNetworkRequest ( url ) ;
request . setHeader ( QNetworkRequest : : ContentTypeHeader , " application/json " ) ;
request . setRawHeader ( " Authorization " , QString ( " Bearer %1 " ) . arg ( m_data - > yggdrasilToken . token ) . toUtf8 ( ) ) ;
2021-09-21 21:02:12 +01:00
AuthRequest * requestor = new AuthRequest ( this ) ;
connect ( requestor , & AuthRequest : : finished , this , & AuthContext : : onMigrationEligibilityCheckDone ) ;
2021-08-29 21:55:33 +01:00
requestor - > get ( request ) ;
}
bool parseRolloutResponse ( QByteArray & data , bool & result ) {
qDebug ( ) < < " Parsing Rollout response... " ;
# ifndef NDEBUG
qDebug ( ) < < data ;
# endif
QJsonParseError jsonError ;
QJsonDocument doc = QJsonDocument : : fromJson ( data , & jsonError ) ;
if ( jsonError . error ) {
qWarning ( ) < < " Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " < < jsonError . errorString ( ) ;
return false ;
}
auto obj = doc . object ( ) ;
QString feature ;
if ( ! getString ( obj . value ( " feature " ) , feature ) ) {
qWarning ( ) < < " Rollout feature is not a string " ;
return false ;
}
if ( feature ! = " msamigration " ) {
qWarning ( ) < < " Rollout feature is not what we expected (msamigration), but is instead \" " < < feature < < " \" " ;
return false ;
}
if ( ! getBool ( obj . value ( " rollout " ) , result ) ) {
qWarning ( ) < < " Rollout feature is not a string " ;
return false ;
}
return true ;
}
2021-08-30 23:55:56 +01:00
void AuthContext : : onMigrationEligibilityCheckDone (
QNetworkReply : : NetworkError error ,
QByteArray data ,
QList < QNetworkReply : : RawHeaderPair > headers
) {
2021-08-29 21:55:33 +01:00
if ( error = = QNetworkReply : : NoError ) {
parseRolloutResponse ( data , m_data - > canMigrateToMSA ) ;
}
2021-07-22 19:15:20 +01:00
doGetSkin ( ) ;
}
2021-07-26 20:44:11 +01:00
void AuthContext : : doGetSkin ( ) {
2021-08-22 19:01:18 +01:00
setStage ( AuthStage : : Skin ) ;
2021-08-15 22:40:37 +01:00
changeState ( STATE_WORKING , tr ( " Fetching player skin " ) ) ;
2021-07-26 20:44:11 +01:00
auto url = QUrl ( m_data - > minecraftProfile . skin . url ) ;
2021-07-22 19:15:20 +01:00
QNetworkRequest request = QNetworkRequest ( url ) ;
2021-09-21 21:02:12 +01:00
AuthRequest * requestor = new AuthRequest ( this ) ;
connect ( requestor , & AuthRequest : : finished , this , & AuthContext : : onSkinDone ) ;
2021-07-22 19:15:20 +01:00
requestor - > get ( request ) ;
}
2021-08-30 23:55:56 +01:00
void AuthContext : : onSkinDone (
QNetworkReply : : NetworkError error ,
QByteArray data ,
QList < QNetworkReply : : RawHeaderPair >
) {
2021-07-22 19:15:20 +01:00
if ( error = = QNetworkReply : : NoError ) {
2021-07-26 20:44:11 +01:00
m_data - > minecraftProfile . skin . data = data ;
2021-07-22 19:15:20 +01:00
}
2021-07-26 20:44:11 +01:00
m_data - > validity_ = Katabasis : : Validity : : Certain ;
2021-07-22 19:15:20 +01:00
finishActivity ( ) ;
2021-08-15 22:40:37 +01:00
changeState ( STATE_SUCCEEDED , tr ( " Finished all authentication steps " ) ) ;
2021-07-22 19:15:20 +01:00
}
2021-08-22 19:01:18 +01:00
void AuthContext : : setStage ( AuthContext : : AuthStage stage ) {
m_stage = stage ;
emit progress ( ( int ) m_stage , ( int ) AuthStage : : Complete ) ;
}
2021-07-26 20:44:11 +01:00
QString AuthContext : : getStateMessage ( ) const {
switch ( m_accountState )
2021-07-22 19:15:20 +01:00
{
2021-07-26 20:44:11 +01:00
case STATE_WORKING :
switch ( m_stage ) {
2021-08-22 19:01:18 +01:00
case AuthStage : : Initial : {
2021-07-26 20:44:11 +01:00
QString loginMessage = tr ( " Logging in as %1 user " ) ;
if ( m_data - > type = = AccountType : : MSA ) {
return loginMessage . arg ( " Microsoft " ) ;
}
else {
return loginMessage . arg ( " Mojang " ) ;
}
}
2021-08-22 19:01:18 +01:00
case AuthStage : : UserAuth :
2021-07-26 20:44:11 +01:00
return tr ( " Logging in as XBox user " ) ;
2021-08-22 19:01:18 +01:00
case AuthStage : : XboxAuth :
2021-07-26 20:44:11 +01:00
return tr ( " Logging in with XBox and Mojang services " ) ;
2021-08-22 19:01:18 +01:00
case AuthStage : : MinecraftProfile :
2021-07-26 20:44:11 +01:00
return tr ( " Getting Minecraft profile " ) ;
2021-08-29 21:55:33 +01:00
case AuthStage : : MigrationEligibility :
return tr ( " Checking for migration eligibility " ) ;
2021-08-22 19:01:18 +01:00
case AuthStage : : Skin :
2021-07-26 20:44:11 +01:00
return tr ( " Getting Minecraft skin " ) ;
2021-08-22 19:01:18 +01:00
case AuthStage : : Complete :
return tr ( " Finished " ) ;
2021-07-26 20:44:11 +01:00
default :
break ;
}
default :
return AccountTask : : getStateMessage ( ) ;
2021-07-22 19:15:20 +01:00
}
}