MSA
This commit is contained in:
parent
779a9a54ec
commit
d8a4d0903a
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
191
app/assets/js/microsoft.js
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
@ -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)
|
||||
})
|
||||
})
|
@ -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>
|
||||
|
47
index.js
47
index.js
@ -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
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user