|
@@ -9,17 +9,19 @@
|
|
|
* @module authmanager
|
|
* @module authmanager
|
|
|
*/
|
|
*/
|
|
|
// Requirements
|
|
// 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 { RestResponseStatus } = require('helios-core/common')
|
|
|
|
|
+const { MojangRestAPI, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang')
|
|
|
|
|
+const { MicrosoftAuth, microsoftErrorDisplayable, MicrosoftErrorCode } = 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 }
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 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) {
|
|
|
|
|
+ try {
|
|
|
|
|
+
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ return Promise.reject(microsoftErrorDisplayable(accessTokenResponse.microsoftErrorCode))
|
|
|
|
|
+ }
|
|
|
|
|
+ accessToken = accessTokenResponse.data
|
|
|
|
|
+ accessTokenRaw = accessToken.access_token
|
|
|
|
|
+ } else {
|
|
|
|
|
+ accessTokenRaw = entryCode
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const xblResponse = await MicrosoftAuth.getXBLToken(accessTokenRaw)
|
|
|
|
|
+ if(xblResponse.responseStatus === RestResponseStatus.ERROR) {
|
|
|
|
|
+ return Promise.reject(microsoftErrorDisplayable(xblResponse.microsoftErrorCode))
|
|
|
|
|
+ }
|
|
|
|
|
+ const xstsResonse = await MicrosoftAuth.getXSTSToken(xblResponse.data)
|
|
|
|
|
+ if(xstsResonse.responseStatus === RestResponseStatus.ERROR) {
|
|
|
|
|
+ return Promise.reject(microsoftErrorDisplayable(xstsResonse.microsoftErrorCode))
|
|
|
|
|
+ }
|
|
|
|
|
+ const mcTokenResponse = await MicrosoftAuth.getMCAccessToken(xstsResonse.data)
|
|
|
|
|
+ if(mcTokenResponse.responseStatus === RestResponseStatus.ERROR) {
|
|
|
|
|
+ return Promise.reject(microsoftErrorDisplayable(mcTokenResponse.microsoftErrorCode))
|
|
|
|
|
+ }
|
|
|
|
|
+ const mcProfileResponse = await MicrosoftAuth.getMCProfile(mcTokenResponse.data.access_token)
|
|
|
|
|
+ if(mcProfileResponse.responseStatus === RestResponseStatus.ERROR) {
|
|
|
|
|
+ return Promise.reject(microsoftErrorDisplayable(mcProfileResponse.microsoftErrorCode))
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ accessToken,
|
|
|
|
|
+ accessTokenRaw,
|
|
|
|
|
+ xbl: xblResponse.data,
|
|
|
|
|
+ xsts: xstsResonse.data,
|
|
|
|
|
+ mcToken: mcTokenResponse.data,
|
|
|
|
|
+ mcProfile: mcProfileResponse.data
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch(err) {
|
|
|
|
|
+ log.error(err)
|
|
|
|
|
+ return Promise.reject(microsoftErrorDisplayable(MicrosoftErrorCode.UNKNOWN))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
- * Remove an account. This will invalidate the access token associated
|
|
|
|
|
|
|
+ * 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)
|
|
@@ -115,4 +232,84 @@ 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()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
}
|
|
}
|