This commit is contained in:
norhu1130 2021-01-13 10:23:40 +09:00
parent 779a9a54ec
commit d8a4d0903a
7 changed files with 420 additions and 20 deletions

View File

@ -858,6 +858,16 @@ body, button {
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
#loginMSButton {
border-color: transparent;
background-color: transparent;
cursor: pointer;
font-family: 'NanumGothicBold';
font-size: 12px;
font-weight: bold;
margin-bottom: 3px;
color: rgba(255, 255, 255, 0.75);
}
/*
#login_filter {

View File

@ -12,9 +12,64 @@
const ConfigManager = require('./configmanager')
const LoggerUtil = require('./loggerutil')
const Mojang = require('./mojang')
const Microsoft = require('./microsoft')
const logger = LoggerUtil('%c[AuthManager]', 'color: #a02d2a; font-weight: bold')
const loggerSuccess = LoggerUtil('%c[AuthManager]', 'color: #209b07; font-weight: bold')
async function validateSelectedMojang() {
const current = ConfigManager.getSelectedAccount()
const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken())
if(!isValid){
try {
const session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken())
ConfigManager.updateAuthAccount(current.uuid, session.accessToken)
ConfigManager.save()
} catch(err) {
logger.debug('Error while validating selected profile:', err)
if(err && err.error === 'ForbiddenOperationException'){
// What do we do?
}
logger.log('Account access token is invalid.')
return false
}
loggerSuccess.log('Account access token validated.')
return true
} else {
loggerSuccess.log('Account access token validated.')
return true
}
}
async function validateSelectedMicrosoft() {
try {
const current = ConfigManager.getSelectedAccount()
const now = new Date().getTime()
const MCExpiresAt = Date.parse(current.expiresAt)
const MCExpired = now > MCExpiresAt
if(MCExpired) {
const MSExpiresAt = Date.parse(ConfigManager.getMicrosoftAuth().expires_at)
const MSExpired = now > MSExpiresAt
if (MSExpired) {
const newAccessToken = await Microsoft.refreshAccessToken(ConfigManager.getMicrosoftAuth)
ConfigManager.updateMicrosoftAuth(newAccessToken.access_token, newAccessToken.expires_at)
ConfigManager.save()
}
const newMCAccessToken = await Microsoft.authMinecraft(ConfigManager.getMicrosoftAuth().access_token)
ConfigManager.updateAuthAccount(current.uuid, newMCAccessToken.access_token, newMCAccessToken.expires_at)
ConfigManager.save()
return true
} else {
return true
}
} catch (error) {
return Promise.reject(error)
}
}
// Exports
// Functions
/**
@ -78,22 +133,31 @@ exports.validateSelected = async function(){
const current = ConfigManager.getSelectedAccount()
const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken())
if(!isValid){
try {
const session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken())
ConfigManager.updateAuthAccount(current.uuid, session.accessToken)
ConfigManager.save()
} catch(err) {
logger.debug('Error while validating selected profile:', err)
if(err && err.error === 'ForbiddenOperationException'){
// What do we do?
}
logger.log('Account access token is invalid.')
return false
}
loggerSuccess.log('Account access token validated.')
return true
try{
if (ConfigManager.getSelectedAccount() === 'microsoft') {
const validate = await validateSelectedMicrosoft()
return validate
} else {
loggerSuccess.log('Account access token validated.')
return true
const validate = await validateSelectedMojang()
return validate
}
} catch (error) {
return Promise.reject(error)
}
}
}
exports.addMSAccount = async authCode => {
try {
const accessToken = await Microsoft.getAccessToken(authCode)
ConfigManager.setMicrosoftAuth(accessToken)
const MCAccessToken = await Microsoft.authMinecraft(accessToken.access_token)
const MCProfile = await Microsoft.getMCProfile(MCAccessToken.access_token)
const ret = ConfigManager.addAuthAccount(MCProfile.id, MCAccessToken.access_token, MCProfile.name, MCProfile.name, MCAccessToken.expires_at, 'microsoft')
ConfigManager.save()
return ret
} catch(error) {
return Promise.reject(error)
}
}

View File

@ -103,7 +103,8 @@ const DEFAULT_CONFIG = {
selectedServer: null, // Resolved
selectedAccount: null,
authenticationDatabase: {},
modConfigurations: []
modConfigurations: [],
microsoftAuth: {}
}
let config = null
@ -327,6 +328,7 @@ exports.getAuthAccount = function(uuid){
*/
exports.updateAuthAccount = function(uuid, accessToken){
config.authenticationDatabase[uuid].accessToken = accessToken
config.authenticationDatabase[uuid].expiresAt = expiresAt
return config.authenticationDatabase[uuid]
}
@ -340,17 +342,18 @@ exports.updateAuthAccount = function(uuid, accessToken){
*
* @returns {Object} The authenticated account object created by this action.
*/
exports.addAuthAccount = function(uuid, accessToken, username, displayName){
exports.addAuthAccount = function(uuid, accessToken, username, displayName, expiresAt = null, type = 'mojang'){
config.selectedAccount = uuid
config.authenticationDatabase[uuid] = {
accessToken,
username: username.trim(),
uuid: uuid.trim(),
displayName: displayName.trim()
displayName: displayName.trim(),
expiresAt: expiresAt,
type: type
}
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
@ -686,3 +689,18 @@ exports.getAllowPrerelease = function(def = false){
exports.setAllowPrerelease = function(allowPrerelease){
config.settings.launcher.allowPrerelease = allowPrerelease
}
exports.setMicrosoftAuth = microsoftAuth => {
config.microsoftAuth = microsoftAuth
}
exports.getMicrosoftAuth = () => {
return config.microsoftAuth
}
exports.updateMicrosoftAuth = (accessToken, expiresAt) => {
config.microsoftAuth.access_token = accessToken
config.microsoftAuth.expires_at = expiresAt
return config.microsoftAuth
}

191
app/assets/js/microsoft.js Normal file
View File

@ -0,0 +1,191 @@
// Requirements
const request = require('request')
// const logger = require('./loggerutil')('%c[Microsoft]', 'color: #01a6f0; font-weight: bold')
// Constants
const clientId = 'Client ID(Azure)'
const tokenUri = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token'
const authXBLUri = 'https://user.auth.xboxlive.com/user/authenticate'
const authXSTSUri = 'https://xsts.auth.xboxlive.com/xsts/authorize'
const authMCUri = 'https://api.minecraftservices.com/authentication/login_with_xbox'
const profileURI ='https://api.minecraftservices.com/minecraft/profile'
// Functions
function requestPromise(uri, options) {
return new Promise((resolve, reject) => {
request(uri, options, (error, response, body) => {
if (error) {
reject(error)
} else if (response.statusCode !== 200){
reject([response.statusCode, response.statusMessage, response])
} else {
resolve(response)
}
})
})
}
function getXBLToken(accessToken) {
return new Promise((resolve, reject) => {
const data = new Object()
const options = {
method: 'post',
json: {
Properties: {
AuthMethod: 'RPS',
SiteName: 'user.auth.xboxlive.com',
RpsTicket: `d=${accessToken}`
},
RelyingParty: 'http://auth.xboxlive.com',
TokenType: 'JWT'
}
}
requestPromise(authXBLUri, options).then(response => {
const body = response.body
data.token = body.Token
data.uhs = body.DisplayClaims.xui[0].uhs
resolve(data)
}).catch(error => {
reject(error)
})
})
}
function getXSTSToken(XBLToken) {
return new Promise((resolve, reject) => {
const options = {
method: 'post',
json: {
Properties: {
SandboxId: 'RETAIL',
UserTokens: [XBLToken]
},
RelyingParty: 'rp://api.minecraftservices.com/',
TokenType: 'JWT'
}
}
requestPromise(authXSTSUri, options).then(response => {
const body = response.body
resolve(body.Token)
}).catch(error => {
reject(error)
})
})
}
function getMCAccessToken(UHS, XSTSToken) {
return new Promise((resolve, reject) => {
const data = new Object()
const expiresAt = new Date()
const options = {
method: 'post',
json: {
identityToken: `XBL3.0 x=${UHS};${XSTSToken}`
}
}
requestPromise(authMCUri, options).then(response => {
const body = response.body
expiresAt.setSeconds(expiresAt.getSeconds() + body.expires_in)
data.access_token = body.access_token
data.expires_at = expiresAt
resolve(data)
}).catch(error => {
reject(error)
})
})
}
// Exports
exports.getAccessToken = authCode => {
return new Promise((resolve, reject) => {
const expiresAt = new Date()
const data = new Object()
const options = {
method: 'post',
formData: {
client_id: clientId,
code: authCode,
scope: 'XboxLive.signin',
redirect_uri: 'https://login.microsoftonline.com/common/oauth2/nativeclient',
grant_type: 'authorization_code'
}
}
requestPromise(tokenUri, options).then(response => {
const body = JSON.parse(response.body)
expiresAt.setSeconds(expiresAt.getSeconds() + body.expires_in)
data.expires_at = expiresAt
data.access_token = body.access_token
data.refresh_token = body.refresh_token
resolve(data)
}).catch(error => {
reject(error)
})
})
}
exports.refreshAccessToken = refreshToken => {
return new Promise((resolve, reject) => {
const expiresAt = new Date()
const data = new Object()
const options = {
method: 'post',
formData: {
client_id: clientId,
refresh_token: refreshToken,
scope: 'XboxLive.signin',
redirect_uri: 'https://login.microsoftonline.com/common/oauth2/nativeclient',
grant_type: 'refresh_token'
}
}
requestPromise(tokenUri, options).then(response => {
const body = JSON.parse(response.body)
expiresAt.setSeconds(expiresAt.getSeconds() + body.expires_in)
data.expires_at = expiresAt
data.access_token = body.access_token
resolve(data)
}).catch(error => {
reject(error)
})
})
}
exports.authMinecraft = async accessToken => {
try {
const XBLToken = await getXBLToken(accessToken)
const XSTSToken = await getXSTSToken(XBLToken.token)
const MCToken = await getMCAccessToken(XBLToken.uhs, XSTSToken)
return MCToken
} catch (error) {
Promise.reject(error)
}
}
exports.getMCProfile = MCAccessToken => {
return new Promise((resolve, reject) => {
const options = {
method: 'get',
headers: {
Authorization: `Bearer ${MCAccessToken}`
}
}
requestPromise(profileURI, options).then(response => {
const body = JSON.parse(response.body)
resolve(body)
}).catch(error => {
reject(error)
})
})
}

View File

@ -17,6 +17,7 @@ const checkmarkContainer = document.getElementById('checkmarkContainer')
const loginRememberOption = document.getElementById('loginRememberOption')
const loginButton = document.getElementById('loginButton')
const loginForm = document.getElementById('loginForm')
const loginMSButton = document.getElementById('loginMSButton')
// Control variables.
let lu = false, lp = false
@ -298,3 +299,71 @@ loginButton.addEventListener('click', () => {
})
})
loginMSButton.addEventListener('click', (event) => {
ipcRenderer.send('openMSALoginWindow', 'open')
})
ipcRenderer.on('MSALoginWindowReply', (event, ...args) => {
if (args[0] === 'error') {
setOverlayContent('LOGIN FAIL', 'Theres a window already open!', 'OK')
setOverlayHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true)
return
}
const queryMap = args[0]
if(queryMap.has('error')) {
let error = queryMap.get('error')
let errorDesc = queryMap.get('error_description')
setOverlayContent(error, errorDesc, 'OK')
setOverlayHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true)
return
}
// Disable form.
formDisabled(true)
// Show loading stuff.
loginLoading(true)
const authCode = queryMap.get('code')
AuthManager.addMSAccount(authCode).then(account => {
updateSelectedAccount(account)
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success'))
$('.circle-loader').toggleClass('load-complete')
$('.checkmark').toggle()
setTimeout(() => {
switchView(VIEWS.login, loginViewOnSuccess, 500, 500, () => {
// Temporary workaround
if(loginViewOnSuccess === VIEWS.settings){
prepareSettings()
}
loginViewOnSuccess = VIEWS.landing // Reset this for good measure.
loginCancelEnabled(false) // Reset this for good measure.
loginViewCancelHandler = null // Reset this for good measure.
loginUsername.value = ''
loginPassword.value = ''
$('.circle-loader').toggleClass('load-complete')
$('.checkmark').toggle()
loginLoading(false)
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login'))
formDisabled(false)
})
}, 1000)
}).catch(error => {
loginLoading(false)
setOverlayContent('ERROR!', 'Report Plz!', Lang.queryJS('login.tryAgain'))
setOverlayHandler(() => {
formDisabled(false)
toggleOverlay(false)
})
toggleOverlay(true)
loggerLogin.error(error)
})
})

View File

@ -9,6 +9,7 @@
<form id="loginForm">
<img id="loginImageSeal" src="assets/images/SealCircle.png"/>
<span id="loginSubheader">MINECRAFT LOGIN</span>
<button id="loginMSButton">OR Microsoft Login</button>
<div class="loginFieldContainer">
<svg id="profileSVG" class="loginSVG" viewBox="40 37 65.36 61.43">
<g>

View File

@ -8,6 +8,8 @@ const path = require('path')
const semver = require('semver')
const url = require('url')
const redirectUriPrefix = 'https://login.microsoftonline.com/common/oauth2/nativeclient?'
// Setup auto updater.
function initAutoUpdater(event, data) {
@ -85,6 +87,49 @@ ipcMain.on('distributionIndexDone', (event, res) => {
// https://electronjs.org/docs/tutorial/offscreen-rendering
app.disableHardwareAcceleration()
let MSALoginWindow = null
// Open the Microsoft Account Login window
ipcMain.on('openMSALoginWindow', (ipcEvent, args) => {
if(MSALoginWindow != null){
ipcEvent.sender.send('MSALoginWindowNotification', 'error', 'AlreadyOpenException')
return
}
MSALoginWindow = new BrowserWindow({
minWidth: 600,
minHeight: 400,
width: 600,
height: 400,
contextIsolation: false
})
MSALoginWindow.on('closed', () => {
MSALoginWindow = null
})
MSALoginWindow.webContents.on('did-navigate', (event, uri, responseCode, statusText) => {
if(uri.startsWith(redirectUriPrefix)) {
let querys = uri.substring(redirectUriPrefix.length).split('#', 1).toString().split('&')
let queryMap = new Map()
querys.forEach(query => {
let arr = query.split('=')
queryMap.set(arr[0], decodeURI(arr[1]))
})
ipcEvent.reply('MSALoginWindowReply', queryMap)
MSALoginWindow.close()
MSALoginWindow = null
}
})
MSALoginWindow.removeMenu()
MSALoginWindow.loadURL('https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=a621aefe-b326-4c67-8688-34746ccd9bd2&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient')
})
// https://github.com/electron/electron/issues/18397
app.allowRendererProcessReuse = true
@ -147,6 +192,8 @@ function createMenu() {
accelerator: 'Command+Q',
click: () => {
app.quit()
if(MSALoginWindow !== null) MSALoginWindow.close()
MSALoginWindow = null
}
}]
}