authmanager.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /**
  2. * AuthManager
  3. *
  4. * This module aims to abstract login procedures. Results from Mojang's REST api
  5. * are retrieved through our Mojang module. These results are processed and stored,
  6. * if applicable, in the config using the ConfigManager. All login procedures should
  7. * be made through this module.
  8. *
  9. * @module authmanager
  10. */
  11. // Requirements
  12. const ConfigManager = require('./configmanager')
  13. const { LoggerUtil } = require('helios-core')
  14. const { RestResponseStatus } = require('helios-core/common')
  15. const { MojangRestAPI, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang')
  16. const { MicrosoftAuth, microsoftErrorDisplayable, MicrosoftErrorCode } = require('helios-core/microsoft')
  17. const { AZURE_CLIENT_ID } = require('./ipcconstants')
  18. const log = LoggerUtil.getLogger('AuthManager')
  19. // Functions
  20. /**
  21. * Add a Mojang account. This will authenticate the given credentials with Mojang's
  22. * authserver. The resultant data will be stored as an auth account in the
  23. * configuration database.
  24. *
  25. * @param {string} username The account username (email if migrated).
  26. * @param {string} password The account password.
  27. * @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
  28. */
  29. exports.addMojangAccount = async function(username, password) {
  30. try {
  31. const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken())
  32. console.log(response)
  33. if(response.responseStatus === RestResponseStatus.SUCCESS) {
  34. const session = response.data
  35. if(session.selectedProfile != null){
  36. const ret = ConfigManager.addMojangAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
  37. if(ConfigManager.getClientToken() == null){
  38. ConfigManager.setClientToken(session.clientToken)
  39. }
  40. ConfigManager.save()
  41. return ret
  42. } else {
  43. return Promise.reject(mojangErrorDisplayable(MojangErrorCode.ERROR_NOT_PAID))
  44. }
  45. } else {
  46. return Promise.reject(mojangErrorDisplayable(response.mojangErrorCode))
  47. }
  48. } catch (err){
  49. log.error(err)
  50. return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN))
  51. }
  52. }
  53. const AUTH_MODE = { FULL: 0, MS_REFRESH: 1, MC_REFRESH: 2 }
  54. /**
  55. * Perform the full MS Auth flow in a given mode.
  56. *
  57. * AUTH_MODE.FULL = Full authorization for a new account.
  58. * AUTH_MODE.MS_REFRESH = Full refresh authorization.
  59. * AUTH_MODE.MC_REFRESH = Refresh of the MC token, reusing the MS token.
  60. *
  61. * @param {string} entryCode FULL-AuthCode. MS_REFRESH=refreshToken, MC_REFRESH=accessToken
  62. * @param {*} authMode The auth mode.
  63. * @returns An object with all auth data. AccessToken object will be null when mode is MC_REFRESH.
  64. */
  65. async function fullMicrosoftAuthFlow(entryCode, authMode) {
  66. try {
  67. let accessTokenRaw
  68. let accessToken
  69. if(authMode !== AUTH_MODE.MC_REFRESH) {
  70. const accessTokenResponse = await MicrosoftAuth.getAccessToken(entryCode, authMode === AUTH_MODE.MS_REFRESH, AZURE_CLIENT_ID)
  71. if(accessTokenResponse.responseStatus === RestResponseStatus.ERROR) {
  72. return Promise.reject(microsoftErrorDisplayable(accessTokenResponse.microsoftErrorCode))
  73. }
  74. accessToken = accessTokenResponse.data
  75. accessTokenRaw = accessToken.access_token
  76. } else {
  77. accessTokenRaw = entryCode
  78. }
  79. const xblResponse = await MicrosoftAuth.getXBLToken(accessTokenRaw)
  80. if(xblResponse.responseStatus === RestResponseStatus.ERROR) {
  81. return Promise.reject(microsoftErrorDisplayable(xblResponse.microsoftErrorCode))
  82. }
  83. const xstsResonse = await MicrosoftAuth.getXSTSToken(xblResponse.data)
  84. if(xstsResonse.responseStatus === RestResponseStatus.ERROR) {
  85. return Promise.reject(microsoftErrorDisplayable(xstsResonse.microsoftErrorCode))
  86. }
  87. const mcTokenResponse = await MicrosoftAuth.getMCAccessToken(xstsResonse.data)
  88. if(mcTokenResponse.responseStatus === RestResponseStatus.ERROR) {
  89. return Promise.reject(microsoftErrorDisplayable(mcTokenResponse.microsoftErrorCode))
  90. }
  91. const mcProfileResponse = await MicrosoftAuth.getMCProfile(mcTokenResponse.data.access_token)
  92. if(mcProfileResponse.responseStatus === RestResponseStatus.ERROR) {
  93. return Promise.reject(microsoftErrorDisplayable(mcProfileResponse.microsoftErrorCode))
  94. }
  95. return {
  96. accessToken,
  97. accessTokenRaw,
  98. xbl: xblResponse.data,
  99. xsts: xstsResonse.data,
  100. mcToken: mcTokenResponse.data,
  101. mcProfile: mcProfileResponse.data
  102. }
  103. } catch(err) {
  104. log.error(err)
  105. return Promise.reject(microsoftErrorDisplayable(MicrosoftErrorCode.UNKNOWN))
  106. }
  107. }
  108. /**
  109. * Calculate the expiry date. Advance the expiry time by 10 seconds
  110. * to reduce the liklihood of working with an expired token.
  111. *
  112. * @param {number} nowMs Current time milliseconds.
  113. * @param {number} epiresInS Expires in (seconds)
  114. * @returns
  115. */
  116. async function calculateExpiryDate(nowMs, epiresInS) {
  117. return nowMs + ((epiresInS-10)*1000)
  118. }
  119. /**
  120. * Add a Microsoft account. This will pass the provided auth code to Mojang's OAuth2.0 flow.
  121. * The resultant data will be stored as an auth account in the configuration database.
  122. *
  123. * @param {string} authCode The authCode obtained from microsoft.
  124. * @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
  125. */
  126. exports.addMicrosoftAccount = async function(authCode) {
  127. const fullAuth = await fullMicrosoftAuthFlow(authCode, AUTH_MODE.FULL)
  128. // Advance expiry by 10 seconds to avoid close calls.
  129. const now = new Date().getTime()
  130. const ret = ConfigManager.addMicrosoftAuthAccount(
  131. fullAuth.mcProfile.id,
  132. fullAuth.mcToken.access_token,
  133. fullAuth.mcProfile.name,
  134. calculateExpiryDate(now, fullAuth.mcToken.expires_in),
  135. fullAuth.accessToken.access_token,
  136. fullAuth.accessToken.refresh_token,
  137. calculateExpiryDate(now, fullAuth.accessToken.expires_in)
  138. )
  139. ConfigManager.save()
  140. return ret
  141. }
  142. /**
  143. * Remove a Mojang account. This will invalidate the access token associated
  144. * with the account and then remove it from the database.
  145. *
  146. * @param {string} uuid The UUID of the account to be removed.
  147. * @returns {Promise.<void>} Promise which resolves to void when the action is complete.
  148. */
  149. exports.removeMojangAccount = async function(uuid){
  150. try {
  151. const authAcc = ConfigManager.getAuthAccount(uuid)
  152. const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken())
  153. if(response.responseStatus === RestResponseStatus.SUCCESS) {
  154. ConfigManager.removeAuthAccount(uuid)
  155. ConfigManager.save()
  156. return Promise.resolve()
  157. } else {
  158. log.error('Error while removing account', response.error)
  159. return Promise.reject(response.error)
  160. }
  161. } catch (err){
  162. log.error('Error while removing account', err)
  163. return Promise.reject(err)
  164. }
  165. }
  166. /**
  167. * Remove a Microsoft account. It is expected that the caller will invoke the OAuth logout
  168. * through the ipc renderer.
  169. *
  170. * @param {string} uuid The UUID of the account to be removed.
  171. * @returns {Promise.<void>} Promise which resolves to void when the action is complete.
  172. */
  173. exports.removeMicrosoftAccount = async function(uuid){
  174. try {
  175. ConfigManager.removeAuthAccount(uuid)
  176. ConfigManager.save()
  177. return Promise.resolve()
  178. } catch (err){
  179. log.error('Error while removing account', err)
  180. return Promise.reject(err)
  181. }
  182. }
  183. /**
  184. * Validate the selected account with Mojang's authserver. If the account is not valid,
  185. * we will attempt to refresh the access token and update that value. If that fails, a
  186. * new login will be required.
  187. *
  188. * @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
  189. * otherwise false.
  190. */
  191. async function validateSelectedMojangAccount(){
  192. const current = ConfigManager.getSelectedAccount()
  193. const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken())
  194. if(response.responseStatus === RestResponseStatus.SUCCESS) {
  195. const isValid = response.data
  196. if(!isValid){
  197. const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken())
  198. if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) {
  199. const session = refreshResponse.data
  200. ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken)
  201. ConfigManager.save()
  202. } else {
  203. log.error('Error while validating selected profile:', refreshResponse.error)
  204. log.info('Account access token is invalid.')
  205. return false
  206. }
  207. log.info('Account access token validated.')
  208. return true
  209. } else {
  210. log.info('Account access token validated.')
  211. return true
  212. }
  213. }
  214. }
  215. /**
  216. * Validate the selected account with Microsoft's authserver. If the account is not valid,
  217. * we will attempt to refresh the access token and update that value. If that fails, a
  218. * new login will be required.
  219. *
  220. * @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
  221. * otherwise false.
  222. */
  223. async function validateSelectedMicrosoftAccount(){
  224. const current = ConfigManager.getSelectedAccount()
  225. const now = new Date().getTime()
  226. const mcExpiresAt = Date.parse(current.expiresAt)
  227. const mcExpired = now >= mcExpiresAt
  228. if(!mcExpired) {
  229. return true
  230. }
  231. // MC token expired. Check MS token.
  232. const msExpiresAt = Date.parse(current.microsoft.expires_at)
  233. const msExpired = now >= msExpiresAt
  234. if(msExpired) {
  235. // MS expired, do full refresh.
  236. try {
  237. const res = await fullMicrosoftAuthFlow(current.microsoft.refresh_token, AUTH_MODE.MS_REFRESH)
  238. ConfigManager.updateMicrosoftAuthAccount(
  239. current.uuid,
  240. res.mcToken.access_token,
  241. res.accessToken.access_token,
  242. res.accessToken.refresh_token,
  243. calculateExpiryDate(now, res.accessToken.expires_in),
  244. calculateExpiryDate(now, res.mcToken.expires_in)
  245. )
  246. ConfigManager.save()
  247. return true
  248. } catch(err) {
  249. return false
  250. }
  251. } else {
  252. // Only MC expired, use existing MS token.
  253. try {
  254. const res = await fullMicrosoftAuthFlow(current.microsoft.access_token, AUTH_MODE.MC_REFRESH)
  255. ConfigManager.updateMicrosoftAuthAccount(
  256. current.uuid,
  257. res.mcToken.access_token,
  258. current.microsoft.access_token,
  259. current.microsoft.refresh_token,
  260. current.microsoft.expires_at,
  261. calculateExpiryDate(now, res.mcToken.expires_in)
  262. )
  263. ConfigManager.save()
  264. return true
  265. }
  266. catch(err) {
  267. return false
  268. }
  269. }
  270. }
  271. /**
  272. * Validate the selected auth account.
  273. *
  274. * @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
  275. * otherwise false.
  276. */
  277. exports.validateSelected = async function(){
  278. const current = ConfigManager.getSelectedAccount()
  279. if(current.type === 'microsoft') {
  280. return await validateSelectedMicrosoftAccount()
  281. } else {
  282. return await validateSelectedMojangAccount()
  283. }
  284. }