| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- /**
- * 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, microsoftErrorDisplayable, MicrosoftErrorCode } = 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) {
- 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))
- }
- }
- /**
- * 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()
- }
-
- }
|