Compare commits

..

3 Commits

Author SHA1 Message Date
Daniel Scalzi
2a99d72dde
Fix settings UI not updating. 2022-02-07 00:55:54 -05:00
Daniel Scalzi
5ea12d469b
Make logout flow more robust. 2022-02-07 00:46:23 -05:00
Daniel Scalzi
eae490b0a7
Microsoft Auth intial implementation.
This commit introduces support for Microsoft Authentication via the settings menu. There are a few known issues and TODOs which should not be hard to fix.
Once the auth flow from the settings screen is complete, the auth folow for first-time login (ie no accounts added yet) must be added. Should not be very difficult. The plan is to add one more view with two login buttons. The Mojang button will just bring you to the login page. The microsoft one will launch OAuth2.
2022-02-06 23:03:06 -05:00
12 changed files with 643 additions and 47 deletions

View File

@ -31,6 +31,7 @@
<div id="main"> <div id="main">
<%- include('welcome') %> <%- include('welcome') %>
<%- include('login') %> <%- include('login') %>
<%- include('waiting') %>
<%- include('settings') %> <%- include('settings') %>
<%- include('landing') %> <%- include('landing') %>
</div> </div>

View File

@ -872,6 +872,85 @@ body, button {
} }
*/ */
/*******************************************************************************
* *
* Waiting View (waiting.ejs) *
* *
******************************************************************************/
#waitingContainer {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
transition: filter 0.25s ease;
background: rgba(0, 0, 0, 0.50);
}
#waitingContent {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 50%;
top: -10%;
position: relative;
}
.waitingSpinner:before {
transform: rotateX(60deg) rotateY(45deg) rotateZ(45deg);
animation: 750ms rotateBefore infinite linear reverse;
}
.waitingSpinner:after {
transform: rotateX(240deg) rotateY(45deg) rotateZ(45deg);
animation: 750ms rotateAfter infinite linear;
}
.waitingSpinner:before,
.waitingSpinner:after {
box-sizing: border-box;
content: '';
display: block;
position: fixed;
top: calc(50% - 5em);
/* left: 50%; */
margin-top: -5em;
margin-left: -5em;
width: 10em;
height: 10em;
transform-style: preserve-3d;
transform-origin: 50%;
transform: rotateY(50%);
perspective-origin: 50% 50%;
perspective: 340px;
background-size: 10em 10em;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI2NnB4IiBoZWlnaHQ9IjI5N3B4IiB2aWV3Qm94PSIwIDAgMjY2IDI5NyIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyI+CiAgICA8dGl0bGU+c3Bpbm5lcjwvdGl0bGU+CiAgICA8ZGVzY3JpcHRpb24+Q3JlYXRlZCB3aXRoIFNrZXRjaCAoaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoKTwvZGVzY3JpcHRpb24+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8cGF0aCBkPSJNMTcxLjUwNzgxMywzLjI1MDAwMDM4IEMyMjYuMjA4MTgzLDEyLjg1NzcxMTEgMjk3LjExMjcyMiw3MS40OTEyODIzIDI1MC44OTU1OTksMTA4LjQxMDE1NSBDMjE2LjU4MjAyNCwxMzUuODIwMzEgMTg2LjUyODQwNSw5Ny4wNjI0OTY0IDE1Ni44MDA3NzQsODUuNzczNDM0NiBDMTI3LjA3MzE0Myw3NC40ODQzNzIxIDc2Ljg4ODQ2MzIsODQuMjE2MTQ2MiA2MC4xMjg5MDY1LDEwOC40MTAxNTMgQy0xNS45ODA0Njg1LDIxOC4yODEyNDcgMTQ1LjI3NzM0NCwyOTYuNjY3OTY4IDE0NS4yNzczNDQsMjk2LjY2Nzk2OCBDMTQ1LjI3NzM0NCwyOTYuNjY3OTY4IC0yNS40NDkyMTg3LDI1Ny4yNDIxOTggMy4zOTg0Mzc1LDEwOC40MTAxNTUgQzE2LjMwNzA2NjEsNDEuODExNDE3NCA4NC43Mjc1ODI5LC0xMS45OTIyOTg1IDE3MS41MDc4MTMsMy4yNTAwMDAzOCBaIiBpZD0iUGF0aC0xIiBmaWxsPSIjZmZmZmZmIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==);
}
#waitingTextContainer {
position: fixed;
top: 50%;
}
@keyframes rotateBefore {
from {
transform: rotateX(60deg) rotateY(45deg) rotateZ(0deg);
}
to {
transform: rotateX(60deg) rotateY(45deg) rotateZ(-360deg);
}
}
@keyframes rotateAfter {
from {
transform: rotateX(240deg) rotateY(45deg) rotateZ(0deg);
}
to {
transform: rotateX(240deg) rotateY(45deg) rotateZ(360deg);
}
}
/******************************************************************************* /*******************************************************************************
* * * *
* Settings View (sttings.ejs) * * Settings View (sttings.ejs) *

View File

@ -11,15 +11,17 @@
// Requirements // Requirements
const ConfigManager = require('./configmanager') const ConfigManager = require('./configmanager')
const { LoggerUtil } = require('helios-core') const { LoggerUtil } = require('helios-core')
const { MojangRestAPI, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang')
const { RestResponseStatus } = require('helios-core/common') const { RestResponseStatus } = require('helios-core/common')
const { MojangRestAPI, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang')
const { MicrosoftAuth } = require('helios-core/microsoft')
const { AZURE_CLIENT_ID } = require('./ipcconstants')
const log = LoggerUtil.getLogger('AuthManager') const log = LoggerUtil.getLogger('AuthManager')
// Functions // Functions
/** /**
* Add an account. This will authenticate the given credentials with Mojang's * Add a Mojang account. This will authenticate the given credentials with Mojang's
* authserver. The resultant data will be stored as an auth account in the * authserver. The resultant data will be stored as an auth account in the
* configuration database. * configuration database.
* *
@ -27,7 +29,7 @@ const log = LoggerUtil.getLogger('AuthManager')
* @param {string} password The account password. * @param {string} password The account password.
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object. * @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
*/ */
exports.addAccount = async function(username, password){ exports.addMojangAccount = async function(username, password) {
try { try {
const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken()) const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken())
console.log(response) console.log(response)
@ -35,7 +37,7 @@ exports.addAccount = async function(username, password){
const session = response.data const session = response.data
if(session.selectedProfile != null){ if(session.selectedProfile != null){
const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name) const ret = ConfigManager.addMojangAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
if(ConfigManager.getClientToken() == null){ if(ConfigManager.getClientToken() == null){
ConfigManager.setClientToken(session.clientToken) ConfigManager.setClientToken(session.clientToken)
} }
@ -55,14 +57,113 @@ exports.addAccount = async function(username, password){
} }
} }
const AUTH_MODE = { FULL: 0, MS_REFRESH: 1, MC_REFRESH: 2 }
/** /**
* Remove an account. This will invalidate the access token associated * Perform the full MS Auth flow in a given mode.
*
* AUTH_MODE.FULL = Full authorization for a new account.
* AUTH_MODE.MS_REFRESH = Full refresh authorization.
* AUTH_MODE.MC_REFRESH = Refresh of the MC token, reusing the MS token.
*
* @param {string} entryCode FULL-AuthCode. MS_REFRESH=refreshToken, MC_REFRESH=accessToken
* @param {*} authMode The auth mode.
* @returns An object with all auth data. AccessToken object will be null when mode is MC_REFRESH.
*/
async function fullMicrosoftAuthFlow(entryCode, authMode) {
let accessTokenRaw
let accessToken
if(authMode !== AUTH_MODE.MC_REFRESH) {
const accessTokenResponse = await MicrosoftAuth.getAccessToken(entryCode, authMode === AUTH_MODE.MS_REFRESH, AZURE_CLIENT_ID)
if(accessTokenResponse.responseStatus === RestResponseStatus.ERROR) {
// TODO Fail.
return // TODO
}
accessToken = accessTokenResponse.data
accessTokenRaw = accessToken.access_token
} else {
accessTokenRaw = entryCode
}
const xblResponse = await MicrosoftAuth.getXBLToken(accessTokenRaw)
if(xblResponse.responseStatus === RestResponseStatus.ERROR) {
// TODO Fail.
return // TODO
}
const xstsResonse = await MicrosoftAuth.getXSTSToken(xblResponse.data)
if(xstsResonse.responseStatus === RestResponseStatus.ERROR) {
// TODO Fail.
return // TODO
}
const mcTokenResponse = await MicrosoftAuth.getMCAccessToken(xstsResonse.data)
if(mcTokenResponse.responseStatus === RestResponseStatus.ERROR) {
// TODO Fail.
return // TODO
}
const mcProfileResponse = await MicrosoftAuth.getMCProfile(mcTokenResponse.data.access_token)
if(mcProfileResponse.responseStatus === RestResponseStatus.ERROR) {
// TODO Fail.
return // TODO
}
return {
accessToken,
accessTokenRaw,
xbl: xblResponse.data,
xsts: xstsResonse.data,
mcToken: mcTokenResponse.data,
mcProfile: mcProfileResponse.data
}
}
/**
* Calculate the expiry date. Advance the expiry time by 10 seconds
* to reduce the liklihood of working with an expired token.
*
* @param {number} nowMs Current time milliseconds.
* @param {number} epiresInS Expires in (seconds)
* @returns
*/
async function calculateExpiryDate(nowMs, epiresInS) {
return nowMs + ((epiresInS-10)*1000)
}
/**
* Add a Microsoft account. This will pass the provided auth code to Mojang's OAuth2.0 flow.
* The resultant data will be stored as an auth account in the configuration database.
*
* @param {string} authCode The authCode obtained from microsoft.
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
*/
exports.addMicrosoftAccount = async function(authCode) {
const fullAuth = await fullMicrosoftAuthFlow(authCode, AUTH_MODE.FULL)
// Advance expiry by 10 seconds to avoid close calls.
const now = new Date().getTime()
const ret = ConfigManager.addMicrosoftAuthAccount(
fullAuth.mcProfile.id,
fullAuth.mcToken.access_token,
fullAuth.mcProfile.name,
calculateExpiryDate(now, fullAuth.mcToken.expires_in),
fullAuth.accessToken.access_token,
fullAuth.accessToken.refresh_token,
calculateExpiryDate(now, fullAuth.accessToken.expires_in)
)
ConfigManager.save()
return ret
}
/**
* Remove a Mojang account. This will invalidate the access token associated
* with the account and then remove it from the database. * with the account and then remove it from the database.
* *
* @param {string} uuid The UUID of the account to be removed. * @param {string} uuid The UUID of the account to be removed.
* @returns {Promise.<void>} Promise which resolves to void when the action is complete. * @returns {Promise.<void>} Promise which resolves to void when the action is complete.
*/ */
exports.removeAccount = async function(uuid){ exports.removeMojangAccount = async function(uuid){
try { try {
const authAcc = ConfigManager.getAuthAccount(uuid) const authAcc = ConfigManager.getAuthAccount(uuid)
const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken()) const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken())
@ -80,17 +181,33 @@ exports.removeAccount = async function(uuid){
} }
} }
/**
* Remove a Microsoft account. It is expected that the caller will invoke the OAuth logout
* through the ipc renderer.
*
* @param {string} uuid The UUID of the account to be removed.
* @returns {Promise.<void>} Promise which resolves to void when the action is complete.
*/
exports.removeMicrosoftAccount = async function(uuid){
try {
ConfigManager.removeAuthAccount(uuid)
ConfigManager.save()
return Promise.resolve()
} catch (err){
log.error('Error while removing account', err)
return Promise.reject(err)
}
}
/** /**
* Validate the selected account with Mojang's authserver. If the account is not valid, * Validate the selected account with Mojang's authserver. If the account is not valid,
* we will attempt to refresh the access token and update that value. If that fails, a * we will attempt to refresh the access token and update that value. If that fails, a
* new login will be required. * new login will be required.
* *
* **Function is WIP**
*
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid, * @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
* otherwise false. * otherwise false.
*/ */
exports.validateSelected = async function(){ async function validateSelectedMojangAccount(){
const current = ConfigManager.getSelectedAccount() const current = ConfigManager.getSelectedAccount()
const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken()) const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken())
@ -100,7 +217,7 @@ exports.validateSelected = async function(){
const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken()) const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken())
if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) { if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) {
const session = refreshResponse.data const session = refreshResponse.data
ConfigManager.updateAuthAccount(current.uuid, session.accessToken) ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken)
ConfigManager.save() ConfigManager.save()
} else { } else {
log.error('Error while validating selected profile:', refreshResponse.error) log.error('Error while validating selected profile:', refreshResponse.error)
@ -116,3 +233,83 @@ exports.validateSelected = async function(){
} }
} }
/**
* Validate the selected account with Microsoft's authserver. If the account is not valid,
* we will attempt to refresh the access token and update that value. If that fails, a
* new login will be required.
*
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
* otherwise false.
*/
async function validateSelectedMicrosoftAccount(){
const current = ConfigManager.getSelectedAccount()
const now = new Date().getTime()
const mcExpiresAt = Date.parse(current.expiresAt)
const mcExpired = now >= mcExpiresAt
if(!mcExpired) {
return true
}
// MC token expired. Check MS token.
const msExpiresAt = Date.parse(current.microsoft.expires_at)
const msExpired = now >= msExpiresAt
if(msExpired) {
// MS expired, do full refresh.
try {
const res = await fullMicrosoftAuthFlow(current.microsoft.refresh_token, AUTH_MODE.MS_REFRESH)
ConfigManager.updateMicrosoftAuthAccount(
current.uuid,
res.mcToken.access_token,
res.accessToken.access_token,
res.accessToken.refresh_token,
calculateExpiryDate(now, res.accessToken.expires_in),
calculateExpiryDate(now, res.mcToken.expires_in)
)
ConfigManager.save()
return true
} catch(err) {
return false
}
} else {
// Only MC expired, use existing MS token.
try {
const res = await fullMicrosoftAuthFlow(current.microsoft.access_token, AUTH_MODE.MC_REFRESH)
ConfigManager.updateMicrosoftAuthAccount(
current.uuid,
res.mcToken.access_token,
current.microsoft.access_token,
current.microsoft.refresh_token,
current.microsoft.expires_at,
calculateExpiryDate(now, res.mcToken.expires_in)
)
ConfigManager.save()
return true
}
catch(err) {
return false
}
}
}
/**
* Validate the selected auth account.
*
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
* otherwise false.
*/
exports.validateSelected = async function(){
const current = ConfigManager.getSelectedAccount()
if(current.type === 'microsoft') {
return await validateSelectedMicrosoftAccount()
} else {
return await validateSelectedMojangAccount()
}
}

View File

@ -318,20 +318,21 @@ exports.getAuthAccount = function(uuid){
} }
/** /**
* Update the access token of an authenticated account. * Update the access token of an authenticated mojang account.
* *
* @param {string} uuid The uuid of the authenticated account. * @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The new Access Token. * @param {string} accessToken The new Access Token.
* *
* @returns {Object} The authenticated account object created by this action. * @returns {Object} The authenticated account object created by this action.
*/ */
exports.updateAuthAccount = function(uuid, accessToken){ exports.updateMojangAuthAccount = function(uuid, accessToken){
config.authenticationDatabase[uuid].accessToken = accessToken config.authenticationDatabase[uuid].accessToken = accessToken
config.authenticationDatabase[uuid].type = 'mojang' // For gradual conversion.
return config.authenticationDatabase[uuid] return config.authenticationDatabase[uuid]
} }
/** /**
* Adds an authenticated account to the database to be stored. * Adds an authenticated mojang account to the database to be stored.
* *
* @param {string} uuid The uuid of the authenticated account. * @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The accessToken of the authenticated account. * @param {string} accessToken The accessToken of the authenticated account.
@ -340,9 +341,10 @@ exports.updateAuthAccount = function(uuid, accessToken){
* *
* @returns {Object} The authenticated account object created by this action. * @returns {Object} The authenticated account object created by this action.
*/ */
exports.addAuthAccount = function(uuid, accessToken, username, displayName){ exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){
config.selectedAccount = uuid config.selectedAccount = uuid
config.authenticationDatabase[uuid] = { config.authenticationDatabase[uuid] = {
type: 'mojang',
accessToken, accessToken,
username: username.trim(), username: username.trim(),
uuid: uuid.trim(), uuid: uuid.trim(),
@ -351,6 +353,58 @@ exports.addAuthAccount = function(uuid, accessToken, username, displayName){
return config.authenticationDatabase[uuid] return config.authenticationDatabase[uuid]
} }
/**
* Update the tokens of an authenticated microsoft account.
*
* @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The new Access Token.
* @param {string} msAccessToken The new Microsoft Access Token
* @param {string} msRefreshToken The new Microsoft Refresh Token
* @param {date} msExpires The date when the microsoft access token expires
* @param {date} mcExpires The date when the mojang access token expires
*
* @returns {Object} The authenticated account object created by this action.
*/
exports.updateMicrosoftAuthAccount = function(uuid, accessToken, msAccessToken, msRefreshToken, msExpires, mcExpires) {
config.authenticationDatabase[uuid].accessToken = accessToken
config.authenticationDatabase[uuid].expiresAt = mcExpires
config.authenticationDatabase[uuid].microsoft.access_token = msAccessToken
config.authenticationDatabase[uuid].microsoft.refresh_token = msRefreshToken
config.authenticationDatabase[uuid].microsoft.expires_at = msExpires
return config.authenticationDatabase[uuid]
}
/**
* Adds an authenticated microsoft account to the database to be stored.
*
* @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The accessToken of the authenticated account.
* @param {string} name The in game name of the authenticated account.
* @param {date} mcExpires The date when the mojang access token expires
* @param {string} msAccessToken The microsoft access token
* @param {string} msRefreshToken The microsoft refresh token
* @param {date} msExpires The date when the microsoft access token expires
*
* @returns {Object} The authenticated account object created by this action.
*/
exports.addMicrosoftAuthAccount = function(uuid, accessToken, name, mcExpires, msAccessToken, msRefreshToken, msExpires) {
config.selectedAccount = uuid
config.authenticationDatabase[uuid] = {
type: 'microsoft',
accessToken,
username: name.trim(),
uuid: uuid.trim(),
displayName: name.trim(),
expiresAt: mcExpires,
microsoft: {
access_token: msAccessToken,
refresh_token: msRefreshToken,
expires_at: msExpires
}
}
return config.authenticationDatabase[uuid]
}
/** /**
* Remove an authenticated account from the database. If the account * Remove an authenticated account from the database. If the account
* was also the selected account, a new one will be selected. If there * was also the selected account, a new one will be selected. If there

View File

@ -0,0 +1,15 @@
exports.AZURE_CLIENT_ID = 'FILL-IN'
exports.MSFT_OPCODE = {
OPEN_LOGIN: 'MSFT_AUTH_OPEN_LOGIN',
OPEN_LOGOUT: 'MSFT_AUTH_OPEN_LOGOUT',
REPLY_LOGIN: 'MSFT_AUTH_REPLY_LOGIN',
REPLY_LOGOUT: 'MSFT_AUTH_REPLY_LOGOUT'
}
exports.MSFT_REPLY_TYPE = {
SUCCESS: 'MSFT_AUTH_REPLY_SUCCESS',
ERROR: 'MSFT_AUTH_REPLY_ERROR'
}
exports.MSFT_ERROR = {
ALREADY_OPEN: 'MSFT_AUTH_ERR_ALREADY_OPEN',
NOT_FINISHED: 'MSFT_AUTH_ERR_NOT_FINISHED'
}

View File

@ -21,7 +21,7 @@ const launch_details_text = document.getElementById('launch_details_text')
const server_selection_button = document.getElementById('server_selection_button') const server_selection_button = document.getElementById('server_selection_button')
const user_text = document.getElementById('user_text') const user_text = document.getElementById('user_text')
const loggerLanding = LoggerUtil('%c[Landing]', 'color: #000668; font-weight: bold') const loggerLanding = LoggerUtil1('%c[Landing]', 'color: #000668; font-weight: bold')
/* Launch Progress Wrapper Functions */ /* Launch Progress Wrapper Functions */
@ -293,7 +293,7 @@ function asyncSystemScan(mcVersion, launchAfter = true){
toggleLaunchArea(true) toggleLaunchArea(true)
setLaunchPercentage(0, 100) setLaunchPercentage(0, 100)
const loggerSysAEx = LoggerUtil('%c[SysAEx]', 'color: #353232; font-weight: bold') const loggerSysAEx = LoggerUtil1('%c[SysAEx]', 'color: #353232; font-weight: bold')
const forkEnv = JSON.parse(JSON.stringify(process.env)) const forkEnv = JSON.parse(JSON.stringify(process.env))
forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory()
@ -495,8 +495,8 @@ function dlAsync(login = true){
toggleLaunchArea(true) toggleLaunchArea(true)
setLaunchPercentage(0, 100) setLaunchPercentage(0, 100)
const loggerAEx = LoggerUtil('%c[AEx]', 'color: #353232; font-weight: bold') const loggerAEx = LoggerUtil1('%c[AEx]', 'color: #353232; font-weight: bold')
const loggerLaunchSuite = LoggerUtil('%c[LaunchSuite]', 'color: #000668; font-weight: bold') const loggerLaunchSuite = LoggerUtil1('%c[LaunchSuite]', 'color: #000668; font-weight: bold')
const forkEnv = JSON.parse(JSON.stringify(process.env)) const forkEnv = JSON.parse(JSON.stringify(process.env))
forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory()

View File

@ -21,7 +21,7 @@ const loginForm = document.getElementById('loginForm')
// Control variables. // Control variables.
let lu = false, lp = false let lu = false, lp = false
const loggerLogin = LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold') const loggerLogin = LoggerUtil1('%c[Login]', 'color: #000668; font-weight: bold')
/** /**
@ -189,7 +189,7 @@ loginButton.addEventListener('click', () => {
// Show loading stuff. // Show loading stuff.
loginLoading(true) loginLoading(true)
AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => { AuthManager.addMojangAccount(loginUsername.value, loginPassword.value).then((value) => {
updateSelectedAccount(value) updateSelectedAccount(value)
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success')) loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success'))
$('.circle-loader').toggleClass('load-complete') $('.circle-loader').toggleClass('load-complete')

View File

@ -4,6 +4,7 @@ const semver = require('semver')
const { JavaGuard } = require('./assets/js/assetguard') const { JavaGuard } = require('./assets/js/assetguard')
const DropinModUtil = require('./assets/js/dropinmodutil') const DropinModUtil = require('./assets/js/dropinmodutil')
const { MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./assets/js/ipcconstants')
const settingsState = { const settingsState = {
invalid: new Set() invalid: new Set()
@ -314,7 +315,10 @@ settingsNavDone.onclick = () => {
* Account Management Tab * Account Management Tab
*/ */
// Bind the add account button. const msftLoginLogger = LoggerUtil.getLogger('Microsoft Login')
const msftLogoutLogger = LoggerUtil.getLogger('Microsoft Logout')
// Bind the add mojang account button.
document.getElementById('settingsAddMojangAccount').onclick = (e) => { document.getElementById('settingsAddMojangAccount').onclick = (e) => {
switchView(getCurrentView(), VIEWS.login, 500, 500, () => { switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
loginViewOnCancel = VIEWS.settings loginViewOnCancel = VIEWS.settings
@ -323,6 +327,73 @@ document.getElementById('settingsAddMojangAccount').onclick = (e) => {
}) })
} }
// Bind the add microsoft account button.
document.getElementById('settingsAddMicrosoftAccount').onclick = (e) => {
switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
ipcRenderer.send(MSFT_OPCODE.OPEN_LOGIN)
})
}
// Bind reply for Microsoft Login.
ipcRenderer.on(MSFT_OPCODE.REPLY_LOGIN, (_, ...arguments_) => {
if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) {
switchView(getCurrentView(), VIEWS.settings, 500, 500, () => {
if(arguments_.length > 1 && arguments_[1] === MSFT_ERROR.NOT_FINISHED) {
// User cancelled.
msftLoginLogger.info('Login cancelled by user.')
return
}
// Unexpected error.
setOverlayContent(
'Something Went Wrong',
'Microsoft authentication failed. Please try again.',
'OK'
)
setOverlayHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true)
})
} else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) {
const queryMap = arguments_[1]
// Error from request to Microsoft.
if (Object.prototype.hasOwnProperty.call(queryMap, 'error')) {
let error = queryMap.error
let errorDesc = queryMap.error_description
title = error
description = errorDesc
if (error === 'access_denied') {
// TODO Write custom error messages.
title = error
description = errorDesc
}
setOverlayContent(
title,
description,
'OK'
)
setOverlayHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true)
} else {
msftLoginLogger.info('Acquired authCode, proceeding with authentication.')
const authCode = queryMap.code
AuthManager.addMicrosoftAccount(authCode).then(value => {
updateSelectedAccount(value)
switchView(getCurrentView(), VIEWS.settings, 500, 500, () => {
prepareSettings()
})
})
}
}
})
/** /**
* Bind functionality for the account selection buttons. If another account * Bind functionality for the account selection buttons. If another account
* is selected, the UI of the previously selected account will be updated. * is selected, the UI of the previously selected account will be updated.
@ -381,6 +452,7 @@ function bindAuthAccountLogOut(){
}) })
} }
let msAccDomElementCache
/** /**
* Process a log out. * Process a log out.
* *
@ -391,7 +463,15 @@ function processLogOut(val, isLastAccount){
const parent = val.closest('.settingsAuthAccount') const parent = val.closest('.settingsAuthAccount')
const uuid = parent.getAttribute('uuid') const uuid = parent.getAttribute('uuid')
const prevSelAcc = ConfigManager.getSelectedAccount() const prevSelAcc = ConfigManager.getSelectedAccount()
AuthManager.removeAccount(uuid).then(() => { const targetAcc = ConfigManager.getAuthAccount(uuid)
if(targetAcc.type === 'microsoft') {
msAccDomElementCache = parent
switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
ipcRenderer.send(MSFT_OPCODE.OPEN_LOGOUT, uuid, isLastAccount)
})
// TODO ADD LOGIC FOR LAST ACCOUNT - SAME AS SOLUTION FOR FIRST TIME LOGIN!
} else {
AuthManager.removeMojangAccount(uuid).then(() => {
if(!isLastAccount && uuid === prevSelAcc.uuid){ if(!isLastAccount && uuid === prevSelAcc.uuid){
const selAcc = ConfigManager.getSelectedAccount() const selAcc = ConfigManager.getSelectedAccount()
refreshAuthAccountSelected(selAcc.uuid) refreshAuthAccountSelected(selAcc.uuid)
@ -403,6 +483,57 @@ function processLogOut(val, isLastAccount){
parent.remove() parent.remove()
}) })
} }
}
// Bind reply for Microsoft Logout.
ipcRenderer.on(MSFT_OPCODE.REPLY_LOGOUT, (_, ...arguments_) => {
if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) {
switchView(getCurrentView(), VIEWS.settings, 500, 500, () => {
if(arguments_.length > 1 && arguments_[1] === MSFT_ERROR.NOT_FINISHED) {
// User cancelled.
msftLogoutLogger.info('Logout cancelled by user.')
return
}
// Unexpected error.
setOverlayContent(
'Something Went Wrong',
'Microsoft logout failed. Please try again.',
'OK'
)
setOverlayHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true)
})
} else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) {
const uuid = arguments_[1]
const isLastAccount = arguments_[2]
const prevSelAcc = ConfigManager.getSelectedAccount()
msftLogoutLogger.info('Logout Successful. uuid:', uuid)
AuthManager.removeMicrosoftAccount(uuid)
.then(() => {
if(!isLastAccount && uuid === prevSelAcc.uuid){
const selAcc = ConfigManager.getSelectedAccount()
refreshAuthAccountSelected(selAcc.uuid)
updateSelectedAccount(selAcc)
validateSelectedAccount()
}
if(msAccDomElementCache) {
msAccDomElementCache.remove()
msAccDomElementCache = null
}
})
.finally(() => {
switchView(getCurrentView(), VIEWS.settings, 500, 500)
})
}
})
/** /**
* Refreshes the status of the selected account on the auth account * Refreshes the status of the selected account on the auth account

View File

@ -18,7 +18,8 @@ const VIEWS = {
landing: '#landingContainer', landing: '#landingContainer',
login: '#loginContainer', login: '#loginContainer',
settings: '#settingsContainer', settings: '#settingsContainer',
welcome: '#welcomeContainer' welcome: '#welcomeContainer',
waiting: '#waitingContainer'
} }
// The currently shown view container. // The currently shown view container.
@ -335,7 +336,7 @@ async function validateSelectedAccount(){
loginViewOnCancel = getCurrentView() loginViewOnCancel = getCurrentView()
if(accLen > 0){ if(accLen > 0){
loginViewCancelHandler = () => { loginViewCancelHandler = () => {
ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName) ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
ConfigManager.save() ConfigManager.save()
validateSelectedAccount() validateSelectedAccount()
} }

View File

@ -9,11 +9,12 @@ const $ = require('jquery')
const {ipcRenderer, shell, webFrame} = require('electron') const {ipcRenderer, shell, webFrame} = require('electron')
const remote = require('@electron/remote') const remote = require('@electron/remote')
const isDev = require('./assets/js/isdev') const isDev = require('./assets/js/isdev')
const LoggerUtil = require('./assets/js/loggerutil') const { LoggerUtil } = require('helios-core')
const LoggerUtil1 = require('./assets/js/loggerutil')
const loggerUICore = LoggerUtil('%c[UICore]', 'color: #000668; font-weight: bold') const loggerUICore = LoggerUtil1('%c[UICore]', 'color: #000668; font-weight: bold')
const loggerAutoUpdater = LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold') const loggerAutoUpdater = LoggerUtil1('%c[AutoUpdater]', 'color: #000668; font-weight: bold')
const loggerAutoUpdaterSuccess = LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold') const loggerAutoUpdaterSuccess = LoggerUtil1('%c[AutoUpdater]', 'color: #209b07; font-weight: bold')
// Log deprecation and process warnings. // Log deprecation and process warnings.
process.traceProcessWarnings = true process.traceProcessWarnings = true

8
app/waiting.ejs Normal file
View File

@ -0,0 +1,8 @@
<div id="waitingContainer" style="display: none;">
<div id="waitingContent">
<div class="waitingSpinner"></div>
<div id="waitingTextContainer">
<h2>Waiting for Microsoft..</h2>
</div>
</div>
</div>

109
index.js
View File

@ -10,6 +10,7 @@ const isDev = require('./app/assets/js/isdev')
const path = require('path') const path = require('path')
const semver = require('semver') const semver = require('semver')
const { pathToFileURL } = require('url') const { pathToFileURL } = require('url')
const { AZURE_CLIENT_ID, MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./app/assets/js/ipcconstants')
// Setup auto updater. // Setup auto updater.
function initAutoUpdater(event, data) { function initAutoUpdater(event, data) {
@ -88,6 +89,114 @@ ipcMain.on('distributionIndexDone', (event, res) => {
// https://electronjs.org/docs/tutorial/offscreen-rendering // https://electronjs.org/docs/tutorial/offscreen-rendering
app.disableHardwareAcceleration() app.disableHardwareAcceleration()
const REDIRECT_URI_PREFIX = 'https://login.microsoftonline.com/common/oauth2/nativeclient?'
// Microsoft Auth Login
let msftAuthWindow
let msftAuthSuccess
ipcMain.on(MSFT_OPCODE.OPEN_LOGIN, (ipcEvent) => {
if (msftAuthWindow) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN)
return
}
msftAuthSuccess = false
msftAuthWindow = new BrowserWindow({
title: 'Microsoft Login',
backgroundColor: '#222222',
width: 520,
height: 600,
frame: true,
icon: getPlatformIcon('SealCircle')
})
msftAuthWindow.on('closed', () => {
msftAuthWindow = undefined
})
msftAuthWindow.on('close', () => {
if(!msftAuthSuccess) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED)
}
})
msftAuthWindow.webContents.on('did-navigate', (_, uri) => {
if (uri.startsWith(REDIRECT_URI_PREFIX)) {
let queries = uri.substring(REDIRECT_URI_PREFIX.length).split('#', 1).toString().split('&')
let queryMap = {}
queries.forEach(query => {
const [name, value] = query.split('=')
queryMap[name] = decodeURI(value)
})
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.SUCCESS, queryMap)
msftAuthSuccess = true
msftAuthWindow.close()
msftAuthWindow = null
}
})
msftAuthWindow.removeMenu()
msftAuthWindow.loadURL(`https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?prompt=select_account&client_id=${AZURE_CLIENT_ID}&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient`)
})
// Microsoft Auth Logout
let msftLogoutWindow
let msftLogoutSuccess
let msftLogoutSuccessSent
ipcMain.on(MSFT_OPCODE.OPEN_LOGOUT, (ipcEvent, uuid, isLastAccount) => {
if (msftLogoutWindow) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN)
return
}
msftLogoutSuccess = false
msftLogoutSuccessSent = false
msftLogoutWindow = new BrowserWindow({
title: 'Microsoft Logout',
backgroundColor: '#222222',
width: 520,
height: 600,
frame: true,
icon: getPlatformIcon('SealCircle')
})
msftLogoutWindow.on('closed', () => {
msftLogoutWindow = undefined
})
msftLogoutWindow.on('close', () => {
if(!msftLogoutSuccess) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED)
} else if(!msftLogoutSuccessSent) {
msftLogoutSuccessSent = true
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
}
})
msftLogoutWindow.webContents.on('did-navigate', (_, uri) => {
if(uri.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession')) {
msftLogoutSuccess = true
setTimeout(() => {
if(!msftLogoutSuccessSent) {
msftLogoutSuccessSent = true
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
}
if(msftLogoutWindow) {
msftLogoutWindow.close()
msftLogoutWindow = null
}
}, 5000)
}
})
msftLogoutWindow.removeMenu()
msftLogoutWindow.loadURL('https://login.microsoftonline.com/common/oauth2/v2.0/logout')
})
// Keep a global reference of the window object, if you don't, the window will // Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected. // be closed automatically when the JavaScript object is garbage collected.
let win let win