mirror of
https://github.com/dscalzi/HeliosLauncher.git
synced 2024-12-22 03:32:12 -08:00
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.
This commit is contained in:
parent
63d15b024e
commit
eae490b0a7
@ -31,6 +31,7 @@
|
||||
<div id="main">
|
||||
<%- include('welcome') %>
|
||||
<%- include('login') %>
|
||||
<%- include('waiting') %>
|
||||
<%- include('settings') %>
|
||||
<%- include('landing') %>
|
||||
</div>
|
||||
|
@ -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) *
|
||||
|
@ -9,17 +9,19 @@
|
||||
* @module authmanager
|
||||
*/
|
||||
// Requirements
|
||||
const ConfigManager = require('./configmanager')
|
||||
const { LoggerUtil } = require('helios-core')
|
||||
const { MojangRestAPI, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang')
|
||||
const ConfigManager = require('./configmanager')
|
||||
const { LoggerUtil } = require('helios-core')
|
||||
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')
|
||||
|
||||
// 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
|
||||
* configuration database.
|
||||
*
|
||||
@ -27,7 +29,7 @@ const log = LoggerUtil.getLogger('AuthManager')
|
||||
* @param {string} password The account password.
|
||||
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
|
||||
*/
|
||||
exports.addAccount = async function(username, password){
|
||||
exports.addMojangAccount = async function(username, password) {
|
||||
try {
|
||||
const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken())
|
||||
console.log(response)
|
||||
@ -35,7 +37,7 @@ exports.addAccount = async function(username, password){
|
||||
|
||||
const session = response.data
|
||||
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){
|
||||
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.
|
||||
*
|
||||
* @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.removeAccount = async function(uuid){
|
||||
exports.removeMojangAccount = async function(uuid){
|
||||
try {
|
||||
const authAcc = ConfigManager.getAuthAccount(uuid)
|
||||
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,
|
||||
* we will attempt to refresh the access token and update that value. If that fails, a
|
||||
* new login will be required.
|
||||
*
|
||||
* **Function is WIP**
|
||||
*
|
||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
||||
* otherwise false.
|
||||
*/
|
||||
exports.validateSelected = async function(){
|
||||
async function validateSelectedMojangAccount(){
|
||||
const current = ConfigManager.getSelectedAccount()
|
||||
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())
|
||||
if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) {
|
||||
const session = refreshResponse.data
|
||||
ConfigManager.updateAuthAccount(current.uuid, session.accessToken)
|
||||
ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken)
|
||||
ConfigManager.save()
|
||||
} else {
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
@ -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} accessToken The new Access Token.
|
||||
*
|
||||
* @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].type = 'mojang' // For gradual conversion.
|
||||
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} 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.
|
||||
*/
|
||||
exports.addAuthAccount = function(uuid, accessToken, username, displayName){
|
||||
exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){
|
||||
config.selectedAccount = uuid
|
||||
config.authenticationDatabase[uuid] = {
|
||||
type: 'mojang',
|
||||
accessToken,
|
||||
username: username.trim(),
|
||||
uuid: uuid.trim(),
|
||||
@ -351,6 +353,58 @@ exports.addAuthAccount = function(uuid, accessToken, username, displayName){
|
||||
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
|
||||
* was also the selected account, a new one will be selected. If there
|
||||
|
15
app/assets/js/ipcconstants.js
Normal file
15
app/assets/js/ipcconstants.js
Normal 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'
|
||||
}
|
@ -189,7 +189,7 @@ loginButton.addEventListener('click', () => {
|
||||
// Show loading stuff.
|
||||
loginLoading(true)
|
||||
|
||||
AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => {
|
||||
AuthManager.addMojangAccount(loginUsername.value, loginPassword.value).then((value) => {
|
||||
updateSelectedAccount(value)
|
||||
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success'))
|
||||
$('.circle-loader').toggleClass('load-complete')
|
||||
|
@ -4,6 +4,7 @@ const semver = require('semver')
|
||||
|
||||
const { JavaGuard } = require('./assets/js/assetguard')
|
||||
const DropinModUtil = require('./assets/js/dropinmodutil')
|
||||
const { MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./assets/js/ipcconstants')
|
||||
|
||||
const settingsState = {
|
||||
invalid: new Set()
|
||||
@ -314,7 +315,7 @@ settingsNavDone.onclick = () => {
|
||||
* Account Management Tab
|
||||
*/
|
||||
|
||||
// Bind the add account button.
|
||||
// Bind the add mojang account button.
|
||||
document.getElementById('settingsAddMojangAccount').onclick = (e) => {
|
||||
switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
|
||||
loginViewOnCancel = VIEWS.settings
|
||||
@ -323,6 +324,74 @@ 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.
|
||||
// TODO Get logger from LoggerUtil
|
||||
console.log('Login Cancelled')
|
||||
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 {
|
||||
|
||||
// TODO Update logging message
|
||||
console.log('Acquired authCode')
|
||||
|
||||
const authCode = queryMap.code
|
||||
AuthManager.addMicrosoftAccount(authCode).then(value => {
|
||||
updateSelectedAccount(value)
|
||||
switchView(getCurrentView(), VIEWS.settings, 500, 500, () => {
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Bind functionality for the account selection buttons. If another account
|
||||
* is selected, the UI of the previously selected account will be updated.
|
||||
@ -391,19 +460,75 @@ function processLogOut(val, isLastAccount){
|
||||
const parent = val.closest('.settingsAuthAccount')
|
||||
const uuid = parent.getAttribute('uuid')
|
||||
const prevSelAcc = ConfigManager.getSelectedAccount()
|
||||
AuthManager.removeAccount(uuid).then(() => {
|
||||
if(!isLastAccount && uuid === prevSelAcc.uuid){
|
||||
const selAcc = ConfigManager.getSelectedAccount()
|
||||
refreshAuthAccountSelected(selAcc.uuid)
|
||||
updateSelectedAccount(selAcc)
|
||||
validateSelectedAccount()
|
||||
}
|
||||
})
|
||||
$(parent).fadeOut(250, () => {
|
||||
parent.remove()
|
||||
})
|
||||
const targetAcc = ConfigManager.getAuthAccount(uuid)
|
||||
if(targetAcc.type === 'microsoft') {
|
||||
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){
|
||||
const selAcc = ConfigManager.getSelectedAccount()
|
||||
refreshAuthAccountSelected(selAcc.uuid)
|
||||
updateSelectedAccount(selAcc)
|
||||
validateSelectedAccount()
|
||||
}
|
||||
})
|
||||
$(parent).fadeOut(250, () => {
|
||||
parent.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Bind reply for Microsoft Logout.
|
||||
ipcRenderer.on(MSFT_OPCODE.REPLY_LOGOUT, (_, ...arguments_) => {
|
||||
console.log('on 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.
|
||||
// TODO Get logger from LoggerUtil
|
||||
console.log('Logout Cancelled')
|
||||
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()
|
||||
|
||||
console.log('Logout Successful. uuid:', uuid)
|
||||
|
||||
AuthManager.removeMicrosoftAccount(uuid)
|
||||
.then(() => {
|
||||
if(!isLastAccount && uuid === prevSelAcc.uuid){
|
||||
const selAcc = ConfigManager.getSelectedAccount()
|
||||
refreshAuthAccountSelected(selAcc.uuid)
|
||||
updateSelectedAccount(selAcc)
|
||||
validateSelectedAccount()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
switchView(getCurrentView(), VIEWS.settings, 500, 500)
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Refreshes the status of the selected account on the auth account
|
||||
* elements.
|
||||
|
@ -18,7 +18,8 @@ const VIEWS = {
|
||||
landing: '#landingContainer',
|
||||
login: '#loginContainer',
|
||||
settings: '#settingsContainer',
|
||||
welcome: '#welcomeContainer'
|
||||
welcome: '#welcomeContainer',
|
||||
waiting: '#waitingContainer'
|
||||
}
|
||||
|
||||
// The currently shown view container.
|
||||
@ -335,7 +336,7 @@ async function validateSelectedAccount(){
|
||||
loginViewOnCancel = getCurrentView()
|
||||
if(accLen > 0){
|
||||
loginViewCancelHandler = () => {
|
||||
ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
|
||||
ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
|
||||
ConfigManager.save()
|
||||
validateSelectedAccount()
|
||||
}
|
||||
|
8
app/waiting.ejs
Normal file
8
app/waiting.ejs
Normal 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>
|
115
index.js
115
index.js
@ -3,13 +3,14 @@ remoteMain.initialize()
|
||||
|
||||
// Requirements
|
||||
const { app, BrowserWindow, ipcMain, Menu } = require('electron')
|
||||
const autoUpdater = require('electron-updater').autoUpdater
|
||||
const ejse = require('ejs-electron')
|
||||
const fs = require('fs')
|
||||
const isDev = require('./app/assets/js/isdev')
|
||||
const path = require('path')
|
||||
const semver = require('semver')
|
||||
const { pathToFileURL } = require('url')
|
||||
const autoUpdater = require('electron-updater').autoUpdater
|
||||
const ejse = require('ejs-electron')
|
||||
const fs = require('fs')
|
||||
const isDev = require('./app/assets/js/isdev')
|
||||
const path = require('path')
|
||||
const semver = require('semver')
|
||||
const { pathToFileURL } = require('url')
|
||||
const { AZURE_CLIENT_ID, MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./app/assets/js/ipcconstants')
|
||||
|
||||
// Setup auto updater.
|
||||
function initAutoUpdater(event, data) {
|
||||
@ -88,6 +89,106 @@ ipcMain.on('distributionIndexDone', (event, res) => {
|
||||
// https://electronjs.org/docs/tutorial/offscreen-rendering
|
||||
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
|
||||
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
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
msftLogoutWindow.webContents.on('did-navigate', () => {
|
||||
setTimeout(() => {
|
||||
if(msftLogoutWindow) {
|
||||
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
|
||||
msftLogoutSuccess = true
|
||||
|
||||
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
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let win
|
||||
|
Loading…
Reference in New Issue
Block a user