mirror of
https://github.com/dscalzi/HeliosLauncher.git
synced 2024-12-23 04:02:14 -08:00
eae490b0a7
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.
315 lines
11 KiB
JavaScript
315 lines
11 KiB
JavaScript
/**
|
|
* AuthManager
|
|
*
|
|
* This module aims to abstract login procedures. Results from Mojang's REST api
|
|
* are retrieved through our Mojang module. These results are processed and stored,
|
|
* if applicable, in the config using the ConfigManager. All login procedures should
|
|
* be made through this module.
|
|
*
|
|
* @module authmanager
|
|
*/
|
|
// Requirements
|
|
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 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.
|
|
*
|
|
* @param {string} username The account username (email if migrated).
|
|
* @param {string} password The account password.
|
|
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
|
|
*/
|
|
exports.addMojangAccount = async function(username, password) {
|
|
try {
|
|
const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken())
|
|
console.log(response)
|
|
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
|
|
|
const session = response.data
|
|
if(session.selectedProfile != null){
|
|
const ret = ConfigManager.addMojangAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
|
|
if(ConfigManager.getClientToken() == null){
|
|
ConfigManager.setClientToken(session.clientToken)
|
|
}
|
|
ConfigManager.save()
|
|
return ret
|
|
} else {
|
|
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.ERROR_NOT_PAID))
|
|
}
|
|
|
|
} else {
|
|
return Promise.reject(mojangErrorDisplayable(response.mojangErrorCode))
|
|
}
|
|
|
|
} catch (err){
|
|
log.error(err)
|
|
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN))
|
|
}
|
|
}
|
|
|
|
const AUTH_MODE = { FULL: 0, MS_REFRESH: 1, MC_REFRESH: 2 }
|
|
|
|
/**
|
|
* 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.removeMojangAccount = async function(uuid){
|
|
try {
|
|
const authAcc = ConfigManager.getAuthAccount(uuid)
|
|
const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken())
|
|
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
|
ConfigManager.removeAuthAccount(uuid)
|
|
ConfigManager.save()
|
|
return Promise.resolve()
|
|
} else {
|
|
log.error('Error while removing account', response.error)
|
|
return Promise.reject(response.error)
|
|
}
|
|
} catch (err){
|
|
log.error('Error while removing account', err)
|
|
return Promise.reject(err)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
|
* otherwise false.
|
|
*/
|
|
async function validateSelectedMojangAccount(){
|
|
const current = ConfigManager.getSelectedAccount()
|
|
const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken())
|
|
|
|
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
|
const isValid = response.data
|
|
if(!isValid){
|
|
const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken())
|
|
if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) {
|
|
const session = refreshResponse.data
|
|
ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken)
|
|
ConfigManager.save()
|
|
} else {
|
|
log.error('Error while validating selected profile:', refreshResponse.error)
|
|
log.info('Account access token is invalid.')
|
|
return false
|
|
}
|
|
log.info('Account access token validated.')
|
|
return true
|
|
} else {
|
|
log.info('Account access token validated.')
|
|
return true
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* 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()
|
|
}
|
|
|
|
} |