diff --git a/README.md b/README.md
index 4d7d6204..93262260 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@
 
 * 🔒 Full account management.
   * Add multiple accounts and easily switch between them.
+  * Microsoft (OAuth 2.0) + Mojang (Yggdrasil) authentication fully supported.
   * Credentials are never stored and transmitted directly to Mojang.
 * 📂 Efficient asset management.
   * Receive client updates as soon as we release them.
@@ -180,13 +181,15 @@ Note that you **cannot** open the DevTools window while using this debug configu
 
 Please give credit to the original author and provide a link to the original source. This is free software, please do at least this much.
 
+For instructions on setting up Microsoft Authentication, see https://github.com/dscalzi/HeliosLauncher/blob/feature/ms-auth/docs/MicrosoftAuth.md.
+
 ---
 
 ## Resources
 
 * [Wiki][wiki]
 * [Nebula (Create Distribution.json)][nebula]
-* [v2 Rewrite Branch (WIP)][v2branch]
+* [v2 Rewrite Branch (Inactive)][v2branch]
 
 The best way to contact the developers is on Discord.
 
diff --git a/app/app.ejs b/app/app.ejs
index 499c10d5..e829fa14 100644
--- a/app/app.ejs
+++ b/app/app.ejs
@@ -31,6 +31,8 @@
     <div id="main">
         <%- include('welcome') %>
         <%- include('login') %>
+        <%- include('waiting') %>
+        <%- include('loginOptions') %>
         <%- include('settings') %>
         <%- include('landing') %>
     </div>
diff --git a/app/assets/css/launcher.css b/app/assets/css/launcher.css
index 56816997..e67984e4 100644
--- a/app/assets/css/launcher.css
+++ b/app/assets/css/launcher.css
@@ -222,6 +222,7 @@ body, button {
     align-items: center;
     height: 100%;
     width: 100%;
+    background: rgba(0, 0, 0, 0.50);
 }
 
 #welcomeContent {
@@ -872,6 +873,175 @@ body, button {
 }
 */
 
+/*******************************************************************************
+ *                                                                             *
+ * Waiting View (waiting.ejs)                                                  *
+ *                                                                             *
+ ******************************************************************************/
+
+#waitingContainer {
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    width: 100%;
+    transition: filter 0.25s ease;
+    background: rgba(0, 0, 0, 0.50);
+}
+
+#waitingContent {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    width: 50%;
+    top: -10%;
+    position: relative;
+}
+
+.waitingSpinner:before {
+    transform: rotateX(60deg) rotateY(45deg) rotateZ(45deg);
+    animation: 750ms rotateBefore infinite linear reverse;
+}
+.waitingSpinner:after {
+    transform: rotateX(240deg) rotateY(45deg) rotateZ(45deg);
+    animation: 750ms rotateAfter infinite linear;
+}
+.waitingSpinner:before,
+.waitingSpinner:after {
+    box-sizing: border-box;
+    content: '';
+    display: block;
+    position: fixed;
+    top: calc(50% - 5em);
+    /* left: 50%; */
+    margin-top: -5em;
+    margin-left: -5em;
+    width: 10em;
+    height: 10em;
+    transform-style: preserve-3d;
+    transform-origin: 50%;
+    transform: rotateY(50%);
+    perspective-origin: 50% 50%;
+    perspective: 340px;
+    background-size: 10em 10em;
+    background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI2NnB4IiBoZWlnaHQ9IjI5N3B4IiB2aWV3Qm94PSIwIDAgMjY2IDI5NyIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyI+CiAgICA8dGl0bGU+c3Bpbm5lcjwvdGl0bGU+CiAgICA8ZGVzY3JpcHRpb24+Q3JlYXRlZCB3aXRoIFNrZXRjaCAoaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoKTwvZGVzY3JpcHRpb24+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8cGF0aCBkPSJNMTcxLjUwNzgxMywzLjI1MDAwMDM4IEMyMjYuMjA4MTgzLDEyLjg1NzcxMTEgMjk3LjExMjcyMiw3MS40OTEyODIzIDI1MC44OTU1OTksMTA4LjQxMDE1NSBDMjE2LjU4MjAyNCwxMzUuODIwMzEgMTg2LjUyODQwNSw5Ny4wNjI0OTY0IDE1Ni44MDA3NzQsODUuNzczNDM0NiBDMTI3LjA3MzE0Myw3NC40ODQzNzIxIDc2Ljg4ODQ2MzIsODQuMjE2MTQ2MiA2MC4xMjg5MDY1LDEwOC40MTAxNTMgQy0xNS45ODA0Njg1LDIxOC4yODEyNDcgMTQ1LjI3NzM0NCwyOTYuNjY3OTY4IDE0NS4yNzczNDQsMjk2LjY2Nzk2OCBDMTQ1LjI3NzM0NCwyOTYuNjY3OTY4IC0yNS40NDkyMTg3LDI1Ny4yNDIxOTggMy4zOTg0Mzc1LDEwOC40MTAxNTUgQzE2LjMwNzA2NjEsNDEuODExNDE3NCA4NC43Mjc1ODI5LC0xMS45OTIyOTg1IDE3MS41MDc4MTMsMy4yNTAwMDAzOCBaIiBpZD0iUGF0aC0xIiBmaWxsPSIjZmZmZmZmIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==);
+}
+
+#waitingTextContainer {
+    position: fixed;
+    top: 50%;
+}
+
+@keyframes rotateBefore {
+    from {
+      transform: rotateX(60deg) rotateY(45deg) rotateZ(0deg);
+    }
+    to {
+      transform: rotateX(60deg) rotateY(45deg) rotateZ(-360deg);
+    }
+}
+
+@keyframes rotateAfter {
+    from {
+      transform: rotateX(240deg) rotateY(45deg) rotateZ(0deg);
+    }
+    to {
+      transform: rotateX(240deg) rotateY(45deg) rotateZ(360deg);
+    }
+}
+
+/*******************************************************************************
+ *                                                                             *
+ * Login Options View (loginOptions.ejs)                                       *
+ *                                                                             *
+ ******************************************************************************/
+
+#loginOptionsContainer {
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    width: 100%;
+    transition: filter 0.25s ease;
+    background: rgba(0, 0, 0, 0.50);
+}
+
+#loginOptionsContent {
+    border-radius: 3px;
+    position: relative;
+    top: -5%;
+}
+
+.loginOptionsMainContent {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+
+.loginOptionActions {
+    display: flex;
+    flex-direction: column;
+    row-gap: 10px;
+}
+
+.loginOptionButtonContainer {
+    width: 16em;
+}
+
+.loginOptionButton {
+    background: rgba(0, 0, 0, 0.25);
+    border: 1px solid rgba(126, 126, 126, 0.57);
+    border-radius: 3px;
+    height: 50px;
+    width: 100%;
+    text-align: left;
+    padding: 0px 25px;
+    cursor: pointer;
+    outline: none;
+    transition: 0.25s ease;
+    display: flex;
+    align-items: center;
+    column-gap: 5px;
+}
+.loginOptionButton:hover,
+.loginOptionButton:focus {
+    background: rgba(54, 54, 54, 0.25);
+    text-shadow: 0px 0px 20px white;
+}
+
+#loginOptionCancelContainer {
+    position: absolute;
+    bottom: -100px;
+}
+
+#loginOptionCancelButton {
+    background: none;
+    border: none;
+    padding: 2px 0px;
+    font-size: 16px;
+    font-weight: bold;
+    color: lightgrey;
+    cursor: pointer;
+    outline: none;
+    transition: 0.25s ease;
+}
+#loginOptionCancelButton:hover,
+#loginOptionCancelButton:focus {
+    text-shadow: 0px 0px 20px lightgrey;
+}
+#loginOptionCancelButton:active {
+    text-shadow: 0px 0px 20px rgba(211, 211, 211, 0.75);
+    color: rgba(211, 211, 211, 0.75);
+}
+#loginOptionCancelButton:disabled {
+    color: rgba(211, 211, 211, 0.75);
+    pointer-events: none;
+}
+
+
 /*******************************************************************************
  *                                                                             *
  * Settings View (sttings.ejs)                                                 *
@@ -1269,45 +1439,65 @@ input:checked + .toggleSwitchSlider:before {
 * Settings View (Account Tab)
 * * */
 
-/* Add account button styles. */
-#settingsAddAccount {
-    background: rgba(0, 0, 0, 0.25);
-    border: 1px solid rgba(126, 126, 126, 0.57);
-    border-radius: 3px;
-    height: 50px;
+.settingsAuthAccountTypeContainer {
+    display: flex;
     width: 75%;
+    flex-direction: column;
+}
+
+.settingsAuthAccountTypeHeader {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    justify-content: space-between;
+    padding: 10px 0px;
+    border-bottom: 1px solid #ffffff85;
+    margin-bottom: 30px;
+}
+
+.settingsAuthAccountTypeHeaderLeft {
+    display: flex;
+    column-gap: 5px;
+}
+
+/* Settings add account button styles. */
+.settingsAddAuthAccount {
+    background: none;
+    border: none;
     text-align: left;
-    padding: 0px 50px;
+    padding: 2px 0px;
+    color: white;
     cursor: pointer;
     outline: none;
     transition: 0.25s ease;
 }
-#settingsAddAccount:hover,
-#settingsAddAccount:focus {
-    background: rgba(54, 54, 54, 0.25);
-    text-shadow: 0px 0px 20px white;
+.settingsAddAuthAccount:hover,
+.settingsAddAuthAccount:focus {
+    text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white;
 }
-
-/* Settings auth accounts header. */
-#settingsCurrentAccountsHeader {
-    margin: 20px 0px;
+.settingsAddAuthAccount:active {
+    text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75);
+    color: rgba(255, 255, 255, 0.75);
+}
+.settingsAddAuthAccount:disabled {
+    color: rgba(255, 255, 255, 0.75);
+    pointer-events: none;
 }
 
 /* Auth account list container styles. */
-#settingsCurrentAccounts {
+.settingsCurrentAccounts {
     margin-bottom: 5%;
 }
-#settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) {
+.settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) {
     margin-bottom: 10px;
 }
-#settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) {
+.settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) {
     margin-top: 10px;
 }
 
 /* Auth account shared styles. */
 .settingsAuthAccount {
     display: flex;
-    width: 75%;
     background: rgba(0, 0, 0, 0.25);
     border-radius: 3px;
     border: 1px solid rgba(126, 126, 126, 0.57);
diff --git a/app/assets/images/icons/microsoft.svg b/app/assets/images/icons/microsoft.svg
new file mode 100644
index 00000000..78a4ed94
--- /dev/null
+++ b/app/assets/images/icons/microsoft.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
+    <path fill="#f3f3f3" d="M0 0h23v23H0z" />
+    <path fill="#f35325" d="M1 1h10v10H1z" />
+    <path fill="#81bc06" d="M12 1h10v10H12z" />
+    <path fill="#05a6f0" d="M1 12h10v10H1z" />
+    <path fill="#ffba08" d="M12 12h10v10H12z" />
+</svg>
\ No newline at end of file
diff --git a/app/assets/images/icons/mojang.svg b/app/assets/images/icons/mojang.svg
new file mode 100644
index 00000000..e1116b41
--- /dev/null
+++ b/app/assets/images/icons/mojang.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 9.677 9.667">
+    <path d="M-26.332-12.098h2.715c-1.357.18-2.574 1.23-2.715 2.633z" fill="#fff" />
+    <path d="M2.598.022h7.07L9.665 7c-.003 1.334-1.113 2.46-2.402 2.654H0V2.542C.134 1.2 1.3.195 2.598.022z" fill="#db2331" />
+    <path d="M1.54 2.844c.314-.76 1.31-.46 1.954-.528.785-.083 1.503.272 2.1.758l.164-.9c.327.345.587.756.964 1.052.28.254.655-.342.86-.013.42.864.408 1.86.54 2.795l-.788-.373C6.9 4.17 5.126 3.052 3.656 3.685c-1.294.592-1.156 2.65.06 3.255 1.354.703 2.953.51 4.405.292-.07.42-.34.87-.834.816l-4.95.002c-.5.055-.886-.413-.838-.89l.04-4.315z" fill="#fff" />
+</svg>
\ No newline at end of file
diff --git a/app/assets/js/authmanager.js b/app/assets/js/authmanager.js
index 5befb727..5ec85289 100644
--- a/app/assets/js/authmanager.js
+++ b/app/assets/js/authmanager.js
@@ -9,17 +9,19 @@
  * @module authmanager
  */
 // 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 { 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 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
  * configuration database.
  * 
@@ -27,7 +29,7 @@ const log = LoggerUtil.getLogger('AuthManager')
  * @param {string} password The account password.
  * @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
  */
-exports.addAccount = async function(username, password){
+exports.addMojangAccount = async function(username, password) {
     try {
         const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken())
         console.log(response)
@@ -35,7 +37,7 @@ exports.addAccount = async function(username, password){
 
             const session = response.data
             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){
                     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 }
+
 /**
- * Remove an account. This will invalidate the access token associated
+ * 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.removeAccount = async function(uuid){
+exports.removeMojangAccount = async function(uuid){
     try {
         const authAcc = ConfigManager.getAuthAccount(uuid)
         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,
  * we will attempt to refresh the access token and update that value. If that fails, a
  * new login will be required.
  * 
- * **Function is WIP**
- * 
  * @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
  * otherwise false.
  */
-exports.validateSelected = async function(){
+async function validateSelectedMojangAccount(){
     const current = ConfigManager.getSelectedAccount()
     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())
             if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) {
                 const session = refreshResponse.data
-                ConfigManager.updateAuthAccount(current.uuid, session.accessToken)
+                ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken)
                 ConfigManager.save()
             } else {
                 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()
+    }
+    
 }
\ No newline at end of file
diff --git a/app/assets/js/configmanager.js b/app/assets/js/configmanager.js
index 2c0bb53c..3dff9502 100644
--- a/app/assets/js/configmanager.js
+++ b/app/assets/js/configmanager.js
@@ -318,20 +318,21 @@ exports.getAuthAccount = function(uuid){
 }
 
 /**
- * Update the access token of an authenticated account.
+ * Update the access token of an authenticated mojang account.
  * 
  * @param {string} uuid The uuid of the authenticated account.
  * @param {string} accessToken The new Access Token.
  * 
  * @returns {Object} The authenticated account object created by this action.
  */
-exports.updateAuthAccount = function(uuid, accessToken){
+exports.updateMojangAuthAccount = function(uuid, accessToken){
     config.authenticationDatabase[uuid].accessToken = accessToken
+    config.authenticationDatabase[uuid].type = 'mojang' // For gradual conversion.
     return config.authenticationDatabase[uuid]
 }
 
 /**
- * Adds an authenticated account to the database to be stored.
+ * Adds an authenticated mojang account to the database to be stored.
  * 
  * @param {string} uuid The uuid of the authenticated account.
  * @param {string} accessToken The accessToken of the authenticated account.
@@ -340,9 +341,10 @@ exports.updateAuthAccount = function(uuid, accessToken){
  * 
  * @returns {Object} The authenticated account object created by this action.
  */
-exports.addAuthAccount = function(uuid, accessToken, username, displayName){
+exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){
     config.selectedAccount = uuid
     config.authenticationDatabase[uuid] = {
+        type: 'mojang',
         accessToken,
         username: username.trim(),
         uuid: uuid.trim(),
@@ -351,6 +353,58 @@ exports.addAuthAccount = function(uuid, accessToken, username, displayName){
     return config.authenticationDatabase[uuid]
 }
 
+/**
+ * Update the tokens of an authenticated microsoft account.
+ * 
+ * @param {string} uuid The uuid of the authenticated account.
+ * @param {string} accessToken The new Access Token.
+ * @param {string} msAccessToken The new Microsoft Access Token
+ * @param {string} msRefreshToken The new Microsoft Refresh Token
+ * @param {date} msExpires The date when the microsoft access token expires
+ * @param {date} mcExpires The date when the mojang access token expires
+ * 
+ * @returns {Object} The authenticated account object created by this action.
+ */
+exports.updateMicrosoftAuthAccount = function(uuid, accessToken, msAccessToken, msRefreshToken, msExpires, mcExpires) {
+    config.authenticationDatabase[uuid].accessToken = accessToken
+    config.authenticationDatabase[uuid].expiresAt = mcExpires
+    config.authenticationDatabase[uuid].microsoft.access_token = msAccessToken
+    config.authenticationDatabase[uuid].microsoft.refresh_token = msRefreshToken
+    config.authenticationDatabase[uuid].microsoft.expires_at = msExpires
+    return config.authenticationDatabase[uuid]
+}
+
+/**
+ * Adds an authenticated microsoft account to the database to be stored.
+ * 
+ * @param {string} uuid The uuid of the authenticated account.
+ * @param {string} accessToken The accessToken of the authenticated account.
+ * @param {string} name The in game name of the authenticated account.
+ * @param {date} mcExpires The date when the mojang access token expires
+ * @param {string} msAccessToken The microsoft access token
+ * @param {string} msRefreshToken The microsoft refresh token
+ * @param {date} msExpires The date when the microsoft access token expires
+ * 
+ * @returns {Object} The authenticated account object created by this action.
+ */
+exports.addMicrosoftAuthAccount = function(uuid, accessToken, name, mcExpires, msAccessToken, msRefreshToken, msExpires) {
+    config.selectedAccount = uuid
+    config.authenticationDatabase[uuid] = {
+        type: 'microsoft',
+        accessToken,
+        username: name.trim(),
+        uuid: uuid.trim(),
+        displayName: name.trim(),
+        expiresAt: mcExpires,
+        microsoft: {
+            access_token: msAccessToken,
+            refresh_token: msRefreshToken,
+            expires_at: msExpires
+        }
+    }
+    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
diff --git a/app/assets/js/ipcconstants.js b/app/assets/js/ipcconstants.js
new file mode 100644
index 00000000..536e948b
--- /dev/null
+++ b/app/assets/js/ipcconstants.js
@@ -0,0 +1,24 @@
+// NOTE FOR THIRD-PARTY
+// REPLACE THIS CLIENT ID WITH YOUR APPLICATION ID.
+// SEE https://github.com/dscalzi/HeliosLauncher/blob/feature/ms-auth/docs/MicrosoftAuth.md
+exports.AZURE_CLIENT_ID = '1ce6e35a-126f-48fd-97fb-54d143ac6d45'
+// SEE NOTE ABOVE.
+
+
+// Opcodes
+exports.MSFT_OPCODE = {
+    OPEN_LOGIN: 'MSFT_AUTH_OPEN_LOGIN',
+    OPEN_LOGOUT: 'MSFT_AUTH_OPEN_LOGOUT',
+    REPLY_LOGIN: 'MSFT_AUTH_REPLY_LOGIN',
+    REPLY_LOGOUT: 'MSFT_AUTH_REPLY_LOGOUT'
+}
+// Reply types for REPLY opcode.
+exports.MSFT_REPLY_TYPE = {
+    SUCCESS: 'MSFT_AUTH_REPLY_SUCCESS',
+    ERROR: 'MSFT_AUTH_REPLY_ERROR'
+}
+// Error types for ERROR reply.
+exports.MSFT_ERROR = {
+    ALREADY_OPEN: 'MSFT_AUTH_ERR_ALREADY_OPEN',
+    NOT_FINISHED: 'MSFT_AUTH_ERR_NOT_FINISHED'
+}
\ No newline at end of file
diff --git a/app/assets/js/scripts/landing.js b/app/assets/js/scripts/landing.js
index 9beabb78..c15896c8 100644
--- a/app/assets/js/scripts/landing.js
+++ b/app/assets/js/scripts/landing.js
@@ -10,7 +10,7 @@ const { MojangRestAPI, getServerStatus }     = require('helios-core/mojang')
 // Internal Requirements
 const DiscordWrapper          = require('./assets/js/discordwrapper')
 const ProcessBuilder          = require('./assets/js/processbuilder')
-const { RestResponseStatus } = require('helios-core/common')
+const { RestResponseStatus, isDisplayableError } = require('helios-core/common')
 
 // Launch Elements
 const launch_content          = document.getElementById('launch_content')
@@ -21,7 +21,7 @@ const launch_details_text     = document.getElementById('launch_details_text')
 const server_selection_button = document.getElementById('server_selection_button')
 const user_text               = document.getElementById('user_text')
 
-const loggerLanding = LoggerUtil('%c[Landing]', 'color: #000668; font-weight: bold')
+const loggerLanding = LoggerUtil1('%c[Landing]', 'color: #000668; font-weight: bold')
 
 /* Launch Progress Wrapper Functions */
 
@@ -293,7 +293,7 @@ function asyncSystemScan(mcVersion, launchAfter = true){
     toggleLaunchArea(true)
     setLaunchPercentage(0, 100)
 
-    const loggerSysAEx = LoggerUtil('%c[SysAEx]', 'color: #353232; font-weight: bold')
+    const loggerSysAEx = LoggerUtil1('%c[SysAEx]', 'color: #353232; font-weight: bold')
 
     const forkEnv = JSON.parse(JSON.stringify(process.env))
     forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory()
@@ -495,8 +495,8 @@ function dlAsync(login = true){
     toggleLaunchArea(true)
     setLaunchPercentage(0, 100)
 
-    const loggerAEx = LoggerUtil('%c[AEx]', 'color: #353232; font-weight: bold')
-    const loggerLaunchSuite = LoggerUtil('%c[LaunchSuite]', 'color: #000668; font-weight: bold')
+    const loggerAEx = LoggerUtil1('%c[AEx]', 'color: #353232; font-weight: bold')
+    const loggerLaunchSuite = LoggerUtil1('%c[LaunchSuite]', 'color: #000668; font-weight: bold')
 
     const forkEnv = JSON.parse(JSON.stringify(process.env))
     forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory()
diff --git a/app/assets/js/scripts/login.js b/app/assets/js/scripts/login.js
index 5cae2fb7..724f09c4 100644
--- a/app/assets/js/scripts/login.js
+++ b/app/assets/js/scripts/login.js
@@ -21,7 +21,7 @@ const loginForm             = document.getElementById('loginForm')
 // Control variables.
 let lu = false, lp = false
 
-const loggerLogin = LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold')
+const loggerLogin = LoggerUtil1('%c[Login]', 'color: #000668; font-weight: bold')
 
 
 /**
@@ -189,7 +189,7 @@ loginButton.addEventListener('click', () => {
     // Show loading stuff.
     loginLoading(true)
 
-    AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => {
+    AuthManager.addMojangAccount(loginUsername.value, loginPassword.value).then((value) => {
         updateSelectedAccount(value)
         loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success'))
         $('.circle-loader').toggleClass('load-complete')
@@ -214,13 +214,26 @@ loginButton.addEventListener('click', () => {
         }, 1000)
     }).catch((displayableError) => {
         loginLoading(false)
-        setOverlayContent(displayableError.title, displayableError.desc, Lang.queryJS('login.tryAgain'))
+
+        let actualDisplayableError
+        if(isDisplayableError(displayableError)) {
+            msftLoginLogger.error('Error while logging in.', displayableError)
+            actualDisplayableError = displayableError
+        } else {
+            // Uh oh.
+            msftLoginLogger.error('Unhandled error during login.', displayableError)
+            actualDisplayableError = {
+                title: 'Unknown Error During Login',
+                desc: 'An unknown error has occurred. Please see the console for details.'
+            }
+        }
+
+        setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain'))
         setOverlayHandler(() => {
             formDisabled(false)
             toggleOverlay(false)
         })
         toggleOverlay(true)
-        loggerLogin.log('Error while logging in.', displayableError)
     })
 
 })
\ No newline at end of file
diff --git a/app/assets/js/scripts/loginOptions.js b/app/assets/js/scripts/loginOptions.js
new file mode 100644
index 00000000..cdb1bc8e
--- /dev/null
+++ b/app/assets/js/scripts/loginOptions.js
@@ -0,0 +1,50 @@
+const loginOptionsCancelContainer = document.getElementById('loginOptionCancelContainer')
+const loginOptionMicrosoft = document.getElementById('loginOptionMicrosoft')
+const loginOptionMojang = document.getElementById('loginOptionMojang')
+const loginOptionsCancelButton = document.getElementById('loginOptionCancelButton')
+
+let loginOptionsCancellable = false
+
+let loginOptionsViewOnLoginSuccess
+let loginOptionsViewOnLoginCancel
+let loginOptionsViewOnCancel
+let loginOptionsViewCancelHandler
+
+function loginOptionsCancelEnabled(val){
+    if(val){
+        $(loginOptionsCancelContainer).show()
+    } else {
+        $(loginOptionsCancelContainer).hide()
+    }
+}
+
+loginOptionMicrosoft.onclick = (e) => {
+    switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
+        ipcRenderer.send(
+            MSFT_OPCODE.OPEN_LOGIN,
+            loginOptionsViewOnLoginSuccess,
+            loginOptionsViewOnLoginCancel
+        )
+    })
+}
+
+loginOptionMojang.onclick = (e) => {
+    switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
+        loginViewOnSuccess = loginOptionsViewOnLoginSuccess
+        loginViewOnCancel = loginOptionsViewOnLoginCancel
+        loginCancelEnabled(true)
+    })
+}
+
+loginOptionsCancelButton.onclick = (e) => {
+    switchView(getCurrentView(), loginOptionsViewOnCancel, 500, 500, () => {
+        // Clear login values (Mojang login)
+        // No cleanup needed for Microsoft.
+        loginUsername.value = ''
+        loginPassword.value = ''
+        if(loginOptionsViewCancelHandler != null){
+            loginOptionsViewCancelHandler()
+            loginOptionsViewCancelHandler = null
+        }
+    })
+}
\ No newline at end of file
diff --git a/app/assets/js/scripts/overlay.js b/app/assets/js/scripts/overlay.js
index 22d81d62..cf2c5c98 100644
--- a/app/assets/js/scripts/overlay.js
+++ b/app/assets/js/scripts/overlay.js
@@ -197,6 +197,9 @@ document.getElementById('accountSelectConfirm').addEventListener('click', () =>
             const authAcc = ConfigManager.setSelectedAccount(listings[i].getAttribute('uuid'))
             ConfigManager.save()
             updateSelectedAccount(authAcc)
+            if(getCurrentView() === VIEWS.settings) {
+                prepareSettings()
+            }
             toggleOverlay(false)
             validateSelectedAccount()
             return
@@ -207,6 +210,9 @@ document.getElementById('accountSelectConfirm').addEventListener('click', () =>
         const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid'))
         ConfigManager.save()
         updateSelectedAccount(authAcc)
+        if(getCurrentView() === VIEWS.settings) {
+            prepareSettings()
+        }
         toggleOverlay(false)
         validateSelectedAccount()
     }
diff --git a/app/assets/js/scripts/settings.js b/app/assets/js/scripts/settings.js
index c70db702..d673cf48 100644
--- a/app/assets/js/scripts/settings.js
+++ b/app/assets/js/scripts/settings.js
@@ -4,6 +4,7 @@ const semver = require('semver')
 
 const { JavaGuard } = require('./assets/js/assetguard')
 const DropinModUtil  = require('./assets/js/dropinmodutil')
+const { MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./assets/js/ipcconstants')
 
 const settingsState = {
     invalid: new Set()
@@ -314,8 +315,11 @@ settingsNavDone.onclick = () => {
  * Account Management Tab
  */
 
-// Bind the add account button.
-document.getElementById('settingsAddAccount').onclick = (e) => {
+const msftLoginLogger = LoggerUtil.getLogger('Microsoft Login')
+const msftLogoutLogger = LoggerUtil.getLogger('Microsoft Logout')
+
+// Bind the add mojang account button.
+document.getElementById('settingsAddMojangAccount').onclick = (e) => {
     switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
         loginViewOnCancel = VIEWS.settings
         loginViewOnSuccess = VIEWS.settings
@@ -323,6 +327,102 @@ document.getElementById('settingsAddAccount').onclick = (e) => {
     })
 }
 
+// Bind the add microsoft account button.
+document.getElementById('settingsAddMicrosoftAccount').onclick = (e) => {
+    switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
+        ipcRenderer.send(MSFT_OPCODE.OPEN_LOGIN, VIEWS.settings, VIEWS.settings)
+    })
+}
+
+// Bind reply for Microsoft Login.
+ipcRenderer.on(MSFT_OPCODE.REPLY_LOGIN, (_, ...arguments_) => {
+    if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) {
+
+        const viewOnClose = arguments_[2]
+        console.log(arguments_)
+        switchView(getCurrentView(), viewOnClose, 500, 500, () => {
+
+            if(arguments_[1] === MSFT_ERROR.NOT_FINISHED) {
+                // User cancelled.
+                msftLoginLogger.info('Login cancelled by user.')
+                return
+            }
+
+            // Unexpected error.
+            setOverlayContent(
+                'Something Went Wrong',
+                'Microsoft authentication failed. Please try again.',
+                'OK'
+            )
+            setOverlayHandler(() => {
+                toggleOverlay(false)
+            })
+            toggleOverlay(true)
+        })
+    } else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) {
+        const queryMap = arguments_[1]
+        const viewOnClose = arguments_[2]
+
+        // Error from request to Microsoft.
+        if (Object.prototype.hasOwnProperty.call(queryMap, 'error')) {
+            switchView(getCurrentView(), viewOnClose, 500, 500, () => {
+                // TODO Dont know what these errors are. Just show them I guess.
+                // This is probably if you messed up the app registration with Azure.
+                console.log('Error getting authCode, is Azure application registered correctly?')
+                console.log(error)
+                console.log(error_description)
+                console.log('Full query map', queryMap)
+                let error = queryMap.error // Error might be 'access_denied' ?
+                let errorDesc = queryMap.error_description
+                setOverlayContent(
+                    error,
+                    errorDesc,
+                    'OK'
+                )
+                setOverlayHandler(() => {
+                    toggleOverlay(false)
+                })
+                toggleOverlay(true)
+
+            })
+        } else {
+
+            msftLoginLogger.info('Acquired authCode, proceeding with authentication.')
+
+            const authCode = queryMap.code
+            AuthManager.addMicrosoftAccount(authCode).then(value => {
+                updateSelectedAccount(value)
+                switchView(getCurrentView(), viewOnClose, 500, 500, () => {
+                    prepareSettings()
+                })
+            })
+                .catch((displayableError) => {
+
+                    let actualDisplayableError
+                    if(isDisplayableError(displayableError)) {
+                        msftLoginLogger.error('Error while logging in.', displayableError)
+                        actualDisplayableError = displayableError
+                    } else {
+                        // Uh oh.
+                        msftLoginLogger.error('Unhandled error during login.', displayableError)
+                        actualDisplayableError = {
+                            title: 'Unknown Error During Login',
+                            desc: 'An unknown error has occurred. Please see the console for details.'
+                        }
+                    }
+
+                    switchView(getCurrentView(), viewOnClose, 500, 500, () => {
+                        setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain'))
+                        setOverlayHandler(() => {
+                            toggleOverlay(false)
+                        })
+                        toggleOverlay(true)
+                    })
+                })
+        }
+    }
+})
+
 /**
  * Bind functionality for the account selection buttons. If another account
  * is selected, the UI of the previously selected account will be updated.
@@ -367,7 +467,6 @@ function bindAuthAccountLogOut(){
                 setOverlayHandler(() => {
                     processLogOut(val, isLastAccount)
                     toggleOverlay(false)
-                    switchView(getCurrentView(), VIEWS.login)
                 })
                 setDismissHandler(() => {
                     toggleOverlay(false)
@@ -381,6 +480,7 @@ function bindAuthAccountLogOut(){
     })
 }
 
+let msAccDomElementCache
 /**
  * Process a log out.
  * 
@@ -391,19 +491,91 @@ function processLogOut(val, isLastAccount){
     const parent = val.closest('.settingsAuthAccount')
     const uuid = parent.getAttribute('uuid')
     const prevSelAcc = ConfigManager.getSelectedAccount()
-    AuthManager.removeAccount(uuid).then(() => {
-        if(!isLastAccount && uuid === prevSelAcc.uuid){
-            const selAcc = ConfigManager.getSelectedAccount()
-            refreshAuthAccountSelected(selAcc.uuid)
-            updateSelectedAccount(selAcc)
-            validateSelectedAccount()
-        }
-    })
-    $(parent).fadeOut(250, () => {
-        parent.remove()
-    })
+    const targetAcc = ConfigManager.getAuthAccount(uuid)
+    if(targetAcc.type === 'microsoft') {
+        msAccDomElementCache = parent
+        switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
+            ipcRenderer.send(MSFT_OPCODE.OPEN_LOGOUT, uuid, isLastAccount)
+        })
+    } else {
+        AuthManager.removeMojangAccount(uuid).then(() => {
+            if(!isLastAccount && uuid === prevSelAcc.uuid){
+                const selAcc = ConfigManager.getSelectedAccount()
+                refreshAuthAccountSelected(selAcc.uuid)
+                updateSelectedAccount(selAcc)
+                validateSelectedAccount()
+            }
+            if(isLastAccount) {
+                loginOptionsCancelEnabled(false)
+                loginOptionsViewOnLoginSuccess = VIEWS.settings
+                loginOptionsViewOnLoginCancel = VIEWS.loginOptions
+                switchView(getCurrentView(), VIEWS.loginOptions)
+            }
+        })
+        $(parent).fadeOut(250, () => {
+            parent.remove()
+        })
+    }
 }
 
+// Bind reply for Microsoft Logout.
+ipcRenderer.on(MSFT_OPCODE.REPLY_LOGOUT, (_, ...arguments_) => {
+    if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) {
+        switchView(getCurrentView(), VIEWS.settings, 500, 500, () => {
+
+            if(arguments_.length > 1 && arguments_[1] === MSFT_ERROR.NOT_FINISHED) {
+                // User cancelled.
+                msftLogoutLogger.info('Logout cancelled by user.')
+                return
+            }
+
+            // Unexpected error.
+            setOverlayContent(
+                'Something Went Wrong',
+                'Microsoft logout failed. Please try again.',
+                'OK'
+            )
+            setOverlayHandler(() => {
+                toggleOverlay(false)
+            })
+            toggleOverlay(true)
+        })
+    } else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) {
+        
+        const uuid = arguments_[1]
+        const isLastAccount = arguments_[2]
+        const prevSelAcc = ConfigManager.getSelectedAccount()
+
+        msftLogoutLogger.info('Logout Successful. uuid:', uuid)
+        
+        AuthManager.removeMicrosoftAccount(uuid)
+            .then(() => {
+                if(!isLastAccount && uuid === prevSelAcc.uuid){
+                    const selAcc = ConfigManager.getSelectedAccount()
+                    refreshAuthAccountSelected(selAcc.uuid)
+                    updateSelectedAccount(selAcc)
+                    validateSelectedAccount()
+                }
+                if(isLastAccount) {
+                    loginOptionsCancelEnabled(false)
+                    loginOptionsViewOnLoginSuccess = VIEWS.settings
+                    loginOptionsViewOnLoginCancel = VIEWS.loginOptions
+                    switchView(getCurrentView(), VIEWS.loginOptions)
+                }
+                if(msAccDomElementCache) {
+                    msAccDomElementCache.remove()
+                    msAccDomElementCache = null
+                }
+            })
+            .finally(() => {
+                if(!isLastAccount) {
+                    switchView(getCurrentView(), VIEWS.settings, 500, 500)
+                }
+            })
+
+    }
+})
+
 /**
  * Refreshes the status of the selected account on the auth account
  * elements.
@@ -425,7 +597,8 @@ function refreshAuthAccountSelected(uuid){
     })
 }
 
-const settingsCurrentAccounts = document.getElementById('settingsCurrentAccounts')
+const settingsCurrentMicrosoftAccounts = document.getElementById('settingsCurrentMicrosoftAccounts')
+const settingsCurrentMojangAccounts = document.getElementById('settingsCurrentMojangAccounts')
 
 /**
  * Add auth account elements for each one stored in the authentication database.
@@ -438,11 +611,13 @@ function populateAuthAccounts(){
     }
     const selectedUUID = ConfigManager.getSelectedAccount().uuid
 
-    let authAccountStr = ''
+    let microsoftAuthAccountStr = ''
+    let mojangAuthAccountStr = ''
 
-    authKeys.map((val) => {
+    authKeys.forEach((val) => {
         const acc = authAccounts[val]
-        authAccountStr += `<div class="settingsAuthAccount" uuid="${acc.uuid}">
+
+        const accHtml = `<div class="settingsAuthAccount" uuid="${acc.uuid}">
             <div class="settingsAuthAccountLeft">
                 <img class="settingsAuthAccountImage" alt="${acc.displayName}" src="https://mc-heads.net/body/${acc.uuid}/60">
             </div>
@@ -465,9 +640,17 @@ function populateAuthAccounts(){
                 </div>
             </div>
         </div>`
+
+        if(acc.type === 'microsoft') {
+            microsoftAuthAccountStr += accHtml
+        } else {
+            mojangAuthAccountStr += accHtml
+        }
+
     })
 
-    settingsCurrentAccounts.innerHTML = authAccountStr
+    settingsCurrentMicrosoftAccounts.innerHTML = microsoftAuthAccountStr
+    settingsCurrentMojangAccounts.innerHTML = mojangAuthAccountStr
 }
 
 /**
diff --git a/app/assets/js/scripts/uibinder.js b/app/assets/js/scripts/uibinder.js
index 0b080d1b..d3385140 100644
--- a/app/assets/js/scripts/uibinder.js
+++ b/app/assets/js/scripts/uibinder.js
@@ -16,9 +16,11 @@ let fatalStartupError = false
 // Mapping of each view to their container IDs.
 const VIEWS = {
     landing: '#landingContainer',
+    loginOptions: '#loginOptionsContainer',
     login: '#loginContainer',
     settings: '#settingsContainer',
-    welcome: '#welcomeContainer'
+    welcome: '#welcomeContainer',
+    waiting: '#waitingContainer'
 }
 
 // The currently shown view container.
@@ -86,8 +88,11 @@ function showMainUI(data){
                 currentView = VIEWS.landing
                 $(VIEWS.landing).fadeIn(1000)
             } else {
-                currentView = VIEWS.login
-                $(VIEWS.login).fadeIn(1000)
+                loginOptionsCancelEnabled(false)
+                loginOptionsViewOnLoginSuccess = VIEWS.landing
+                loginOptionsViewOnLoginCancel = VIEWS.loginOptions
+                currentView = VIEWS.loginOptions
+                $(VIEWS.loginOptions).fadeIn(1000)
             }
         }
 
@@ -329,20 +334,46 @@ async function validateSelectedAccount(){
                 'Select Another Account'
             )
             setOverlayHandler(() => {
-                document.getElementById('loginUsername').value = selectedAcc.username
-                validateEmail(selectedAcc.username)
-                loginViewOnSuccess = getCurrentView()
-                loginViewOnCancel = getCurrentView()
-                if(accLen > 0){
-                    loginViewCancelHandler = () => {
-                        ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
+
+                const isMicrosoft = selectedAcc.type === 'microsoft'
+
+                if(isMicrosoft) {
+                    // Empty for now
+                } else {
+                    // Mojang
+                    // For convenience, pre-populate the username of the account.
+                    document.getElementById('loginUsername').value = selectedAcc.username
+                    validateEmail(selectedAcc.username)
+                }
+                
+                loginOptionsViewOnLoginSuccess = getCurrentView()
+                loginOptionsViewOnLoginCancel = VIEWS.loginOptions
+
+                if(accLen > 0) {
+                    loginOptionsViewOnCancel = getCurrentView()
+                    loginOptionsViewCancelHandler = () => {
+                        if(isMicrosoft) {
+                            ConfigManager.addMicrosoftAuthAccount(
+                                selectedAcc.uuid,
+                                selectedAcc.accessToken,
+                                selectedAcc.username,
+                                selectedAcc.expiresAt,
+                                selectedAcc.microsoft.access_token,
+                                selectedAcc.microsoft.refresh_token,
+                                selectedAcc.microsoft.expires_at
+                            )
+                        } else {
+                            ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
+                        }
                         ConfigManager.save()
                         validateSelectedAccount()
                     }
-                    loginCancelEnabled(true)
+                    loginOptionsCancelEnabled(true)
+                } else {
+                    loginOptionsCancelEnabled(false)
                 }
                 toggleOverlay(false)
-                switchView(getCurrentView(), VIEWS.login)
+                switchView(getCurrentView(), VIEWS.loginOptions)
             })
             setDismissHandler(() => {
                 if(accLen > 1){
diff --git a/app/assets/js/scripts/uicore.js b/app/assets/js/scripts/uicore.js
index 7d3cddbd..cb36c9d5 100644
--- a/app/assets/js/scripts/uicore.js
+++ b/app/assets/js/scripts/uicore.js
@@ -9,11 +9,12 @@ const $                              = require('jquery')
 const {ipcRenderer, shell, webFrame} = require('electron')
 const remote                         = require('@electron/remote')
 const isDev                          = require('./assets/js/isdev')
-const LoggerUtil                     = require('./assets/js/loggerutil')
+const { LoggerUtil }                 = require('helios-core')
+const LoggerUtil1                    = require('./assets/js/loggerutil')
 
-const loggerUICore             = LoggerUtil('%c[UICore]', 'color: #000668; font-weight: bold')
-const loggerAutoUpdater        = LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold')
-const loggerAutoUpdaterSuccess = LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold')
+const loggerUICore             = LoggerUtil1('%c[UICore]', 'color: #000668; font-weight: bold')
+const loggerAutoUpdater        = LoggerUtil1('%c[AutoUpdater]', 'color: #000668; font-weight: bold')
+const loggerAutoUpdaterSuccess = LoggerUtil1('%c[AutoUpdater]', 'color: #209b07; font-weight: bold')
 
 // Log deprecation and process warnings.
 process.traceProcessWarnings = true
diff --git a/app/assets/js/scripts/welcome.js b/app/assets/js/scripts/welcome.js
index e6ff6297..ed0399c3 100644
--- a/app/assets/js/scripts/welcome.js
+++ b/app/assets/js/scripts/welcome.js
@@ -2,5 +2,8 @@
  * Script for welcome.ejs
  */
 document.getElementById('welcomeButton').addEventListener('click', e => {
-    switchView(VIEWS.welcome, VIEWS.login)
+    loginOptionsCancelEnabled(false) // False by default, be explicit.
+    loginOptionsViewOnLoginSuccess = VIEWS.landing
+    loginOptionsViewOnLoginCancel = VIEWS.loginOptions
+    switchView(VIEWS.welcome, VIEWS.loginOptions)
 })
\ No newline at end of file
diff --git a/app/loginOptions.ejs b/app/loginOptions.ejs
new file mode 100644
index 00000000..36af37e0
--- /dev/null
+++ b/app/loginOptions.ejs
@@ -0,0 +1,34 @@
+<div id="loginOptionsContainer" style="display: none;">
+    <div id="loginOptionsContent">
+        <div class="loginOptionsMainContent">
+            <h2>Login Options</h2>
+            <div class="loginOptionActions">
+                <div class="loginOptionButtonContainer">
+                    <button id="loginOptionMicrosoft" class="loginOptionButton">
+                        <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 23 23">
+                            <path fill="#f35325" d="M1 1h10v10H1z" />
+                            <path fill="#81bc06" d="M12 1h10v10H12z" />
+                            <path fill="#05a6f0" d="M1 12h10v10H1z" />
+                            <path fill="#ffba08" d="M12 12h10v10H12z" />
+                        </svg>
+                        <span>Login with Microsoft</span>
+                    </button>
+                </div>
+                <div class="loginOptionButtonContainer">
+                    <button id="loginOptionMojang" class="loginOptionButton">
+                        <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 9.677 9.667">
+                            <path d="M-26.332-12.098h2.715c-1.357.18-2.574 1.23-2.715 2.633z" fill="#fff" />
+                            <path d="M2.598.022h7.07L9.665 7c-.003 1.334-1.113 2.46-2.402 2.654H0V2.542C.134 1.2 1.3.195 2.598.022z" fill="#db2331" />
+                            <path d="M1.54 2.844c.314-.76 1.31-.46 1.954-.528.785-.083 1.503.272 2.1.758l.164-.9c.327.345.587.756.964 1.052.28.254.655-.342.86-.013.42.864.408 1.86.54 2.795l-.788-.373C6.9 4.17 5.126 3.052 3.656 3.685c-1.294.592-1.156 2.65.06 3.255 1.354.703 2.953.51 4.405.292-.07.42-.34.87-.834.816l-4.95.002c-.5.055-.886-.413-.838-.89l.04-4.315z" fill="#fff" />
+                        </svg>
+                        <span>Login with Mojang</span>
+                    </button>
+                </div>
+            </div>
+            <div id="loginOptionCancelContainer" style="display: none;">
+                <button id="loginOptionCancelButton">Cancel</button>
+            </div>
+        </div>
+    </div>
+    <script src="./assets/js/scripts/loginOptions.js"></script>
+</div>
\ No newline at end of file
diff --git a/app/settings.ejs b/app/settings.ejs
index f5505cfe..65a1796d 100644
--- a/app/settings.ejs
+++ b/app/settings.ejs
@@ -28,16 +28,45 @@
                 <span class="settingsTabHeaderText">Account Settings</span>
                 <span class="settingsTabHeaderDesc">Add new accounts or manage existing ones.</span>
             </div>
-            <div id="settingsAddAccountContainer">
-                <button id="settingsAddAccount">
-                    <span id="settingsAddAccountText">&#43; Add Account</span>
-                </button>
+            <div class="settingsAuthAccountTypeContainer">
+                <div class="settingsAuthAccountTypeHeader">
+                    <div class="settingsAuthAccountTypeHeaderLeft">
+                        <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 23 23">
+                            <path fill="#f35325" d="M1 1h10v10H1z" />
+                            <path fill="#81bc06" d="M12 1h10v10H12z" />
+                            <path fill="#05a6f0" d="M1 12h10v10H1z" />
+                            <path fill="#ffba08" d="M12 12h10v10H12z" />
+                        </svg>
+                        <span>Microsoft</span>
+                    </div>
+                    <div class="settingsAuthAccountTypeHeaderRight">
+                        <button class="settingsAddAuthAccount" id="settingsAddMicrosoftAccount">+ Add Microsoft Account</button>
+                    </div>
+                </div>
+                
+                <div class="settingsCurrentAccounts" id="settingsCurrentMicrosoftAccounts">
+                    <!-- Microsoft auth accounts populated here. -->
+                </div>
             </div>
-            <div id="settingsCurrentAccountsHeader">
-                <span class="settingsFieldTitle">Current Accounts</span>
-            </div>
-            <div id="settingsCurrentAccounts">
-                <!-- Auth accounts populated here. -->
+
+            <div class="settingsAuthAccountTypeContainer">
+                <div class="settingsAuthAccountTypeHeader">
+                    <div class="settingsAuthAccountTypeHeaderLeft">
+                        <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 9.677 9.667">
+                            <path d="M-26.332-12.098h2.715c-1.357.18-2.574 1.23-2.715 2.633z" fill="#fff" />
+                            <path d="M2.598.022h7.07L9.665 7c-.003 1.334-1.113 2.46-2.402 2.654H0V2.542C.134 1.2 1.3.195 2.598.022z" fill="#db2331" />
+                            <path d="M1.54 2.844c.314-.76 1.31-.46 1.954-.528.785-.083 1.503.272 2.1.758l.164-.9c.327.345.587.756.964 1.052.28.254.655-.342.86-.013.42.864.408 1.86.54 2.795l-.788-.373C6.9 4.17 5.126 3.052 3.656 3.685c-1.294.592-1.156 2.65.06 3.255 1.354.703 2.953.51 4.405.292-.07.42-.34.87-.834.816l-4.95.002c-.5.055-.886-.413-.838-.89l.04-4.315z" fill="#fff" />
+                        </svg>
+                        <span>Mojang</span>
+                    </div>
+                    <div class="settingsAuthAccountTypeHeaderRight">
+                        <button class="settingsAddAuthAccount" id="settingsAddMojangAccount">+ Add Mojang Account</button>
+                    </div>
+                </div>
+                
+                <div class="settingsCurrentAccounts" id="settingsCurrentMojangAccounts">
+                    <!-- Mojang auth accounts populated here. -->
+                </div>
             </div>
         </div>
         <div id="settingsTabMinecraft" class="settingsTab" style="display: none;">
diff --git a/app/waiting.ejs b/app/waiting.ejs
new file mode 100644
index 00000000..11c7e4d2
--- /dev/null
+++ b/app/waiting.ejs
@@ -0,0 +1,8 @@
+<div id="waitingContainer" style="display: none;">
+    <div id="waitingContent">
+        <div class="waitingSpinner"></div>
+        <div id="waitingTextContainer">
+            <h2>Waiting for Microsoft..</h2>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/docs/MicrosoftAuth.md b/docs/MicrosoftAuth.md
new file mode 100644
index 00000000..589a75b5
--- /dev/null
+++ b/docs/MicrosoftAuth.md
@@ -0,0 +1,35 @@
+# Microsoft Authentication
+
+Authenticating with Microsoft is fully supported by Helios Launcher.
+
+## Acquiring an Azure Client ID
+
+1. Navigate to https://portal.azure.com
+2. In the search bar, search for **Azure Active Directory**.
+3. In Azure Active Directory, go to **App Registrations** on the left pane (Under *Manage*).
+4. Click **New Registration**.
+    - Set **Name** to be your launcher's name.
+    - Set **Supported account types** to *Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)*
+    - Leave **Redirect URI** blank.
+    - Register the application.
+5. You should be on the application's management page. If not, Navigate back to **App Registrations**. Select the application you just registered.
+6. Click **Authentication** on the left pane (Under *Manage*).
+7. Click **Add Platform**.
+    - Select **Mobile and desktop applications**.
+    - Choose `https://login.microsoftonline.com/common/oauth2/nativeclient` as the **Redirect URI**.
+    - Select **Configure** to finish adding the platform.
+8. Navigate back to **Overview**.
+9. Copy **Application (client) ID**.
+
+
+Reference: https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
+
+## Adding the Azure Client ID to Helios Launcher.
+
+In `app/assets/js/ipcconstants.js` you'll find **`AZURE_CLIENT_ID`**. Set it to your application's id.
+
+Note: Azure Client ID is NOT a secret value and **can** be stored in git. Reference: https://stackoverflow.com/questions/57306964/are-azure-active-directorys-tenantid-and-clientid-considered-secrets
+
+----
+
+You can now authenticate with Microsoft through the launcher.
\ No newline at end of file
diff --git a/index.js b/index.js
index 08d6be35..91120ee9 100644
--- a/index.js
+++ b/index.js
@@ -3,13 +3,14 @@ remoteMain.initialize()
 
 // Requirements
 const { app, BrowserWindow, ipcMain, Menu } = require('electron')
-const autoUpdater                   = require('electron-updater').autoUpdater
-const ejse                          = require('ejs-electron')
-const fs                            = require('fs')
-const isDev                         = require('./app/assets/js/isdev')
-const path                          = require('path')
-const semver                        = require('semver')
-const { pathToFileURL }             = require('url')
+const autoUpdater                       = require('electron-updater').autoUpdater
+const ejse                              = require('ejs-electron')
+const fs                                = require('fs')
+const isDev                             = require('./app/assets/js/isdev')
+const path                              = require('path')
+const semver                            = require('semver')
+const { pathToFileURL }                 = require('url')
+const { AZURE_CLIENT_ID, MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./app/assets/js/ipcconstants')
 
 // Setup auto updater.
 function initAutoUpdater(event, data) {
@@ -88,6 +89,118 @@ ipcMain.on('distributionIndexDone', (event, res) => {
 // https://electronjs.org/docs/tutorial/offscreen-rendering
 app.disableHardwareAcceleration()
 
+
+const REDIRECT_URI_PREFIX = 'https://login.microsoftonline.com/common/oauth2/nativeclient?'
+
+// Microsoft Auth Login
+let msftAuthWindow
+let msftAuthSuccess
+let msftAuthViewSuccess
+let msftAuthViewOnClose
+ipcMain.on(MSFT_OPCODE.OPEN_LOGIN, (ipcEvent, ...arguments_) => {
+    if (msftAuthWindow) {
+        ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN, msftAuthViewOnClose)
+        return
+    }
+    msftAuthSuccess = false
+    msftAuthViewSuccess = arguments_[0]
+    msftAuthViewOnClose = arguments_[1]
+    msftAuthWindow = new BrowserWindow({
+        title: 'Microsoft Login',
+        backgroundColor: '#222222',
+        width: 520,
+        height: 600,
+        frame: true,
+        icon: getPlatformIcon('SealCircle')
+    })
+
+    msftAuthWindow.on('closed', () => {
+        msftAuthWindow = undefined
+    })
+
+    msftAuthWindow.on('close', () => {
+        if(!msftAuthSuccess) {
+            ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED, msftAuthViewOnClose)
+        }
+    })
+
+    msftAuthWindow.webContents.on('did-navigate', (_, uri) => {
+        if (uri.startsWith(REDIRECT_URI_PREFIX)) {
+            let queries = uri.substring(REDIRECT_URI_PREFIX.length).split('#', 1).toString().split('&')
+            let queryMap = {}
+
+            queries.forEach(query => {
+                const [name, value] = query.split('=')
+                queryMap[name] = decodeURI(value)
+            })
+
+            ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.SUCCESS, queryMap, msftAuthViewSuccess)
+
+            msftAuthSuccess = true
+            msftAuthWindow.close()
+            msftAuthWindow = null
+        }
+    })
+
+    msftAuthWindow.removeMenu()
+    msftAuthWindow.loadURL(`https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?prompt=select_account&client_id=${AZURE_CLIENT_ID}&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient`)
+})
+
+// Microsoft Auth Logout
+let msftLogoutWindow
+let msftLogoutSuccess
+let msftLogoutSuccessSent
+ipcMain.on(MSFT_OPCODE.OPEN_LOGOUT, (ipcEvent, uuid, isLastAccount) => {
+    if (msftLogoutWindow) {
+        ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN)
+        return
+    }
+
+    msftLogoutSuccess = false
+    msftLogoutSuccessSent = false
+    msftLogoutWindow = new BrowserWindow({
+        title: 'Microsoft Logout',
+        backgroundColor: '#222222',
+        width: 520,
+        height: 600,
+        frame: true,
+        icon: getPlatformIcon('SealCircle')
+    })
+
+    msftLogoutWindow.on('closed', () => {
+        msftLogoutWindow = undefined
+    })
+
+    msftLogoutWindow.on('close', () => {
+        if(!msftLogoutSuccess) {
+            ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED)
+        } else if(!msftLogoutSuccessSent) {
+            msftLogoutSuccessSent = true
+            ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
+        }
+    })
+    
+    msftLogoutWindow.webContents.on('did-navigate', (_, uri) => {
+        if(uri.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession')) {
+            msftLogoutSuccess = true
+            setTimeout(() => {
+                if(!msftLogoutSuccessSent) {
+                    msftLogoutSuccessSent = true
+                    ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
+                }
+
+                if(msftLogoutWindow) {
+                    msftLogoutWindow.close()
+                    msftLogoutWindow = null
+                }
+            }, 5000)
+        }
+    })
+    
+    msftLogoutWindow.removeMenu()
+    msftLogoutWindow.loadURL('https://login.microsoftonline.com/common/oauth2/v2.0/logout')
+})
+
 // Keep a global reference of the window object, if you don't, the window will
 // be closed automatically when the JavaScript object is garbage collected.
 let win
diff --git a/package-lock.json b/package-lock.json
index 9bf429f4..e84b8f57 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,7 +19,7 @@
         "fs-extra": "^10.0.0",
         "github-syntax-dark": "^0.5.0",
         "got": "^11.8.3",
-        "helios-core": "^0.1.0-alpha.5",
+        "helios-core": "~0.1.0",
         "jquery": "^3.6.0",
         "node-stream-zip": "^1.15.0",
         "request": "^2.88.2",
@@ -2308,9 +2308,9 @@
       }
     },
     "node_modules/helios-core": {
-      "version": "0.1.0-alpha.5",
-      "resolved": "https://registry.npmjs.org/helios-core/-/helios-core-0.1.0-alpha.5.tgz",
-      "integrity": "sha512-Ml6XNOg3lVmGXpvi3N+my01JW1QkzeghT5oQ3yU0Cby7R1az6z1kuz5UN2VuQpzsFeQtgqeTmDPQDOlXdvw9Nw==",
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/helios-core/-/helios-core-0.1.0.tgz",
+      "integrity": "sha512-p2jmVSeciR9wcKLPc5seMxU0YsURF8ttLAIJS1CHU5fyoe40F3GXWPhsSdbGiDSRjLUcOzpjea1WTyGJ0zAgEA==",
       "dependencies": {
         "fs-extra": "^10.0.0",
         "got": "^11.8.3",
@@ -6376,9 +6376,9 @@
       "dev": true
     },
     "helios-core": {
-      "version": "0.1.0-alpha.5",
-      "resolved": "https://registry.npmjs.org/helios-core/-/helios-core-0.1.0-alpha.5.tgz",
-      "integrity": "sha512-Ml6XNOg3lVmGXpvi3N+my01JW1QkzeghT5oQ3yU0Cby7R1az6z1kuz5UN2VuQpzsFeQtgqeTmDPQDOlXdvw9Nw==",
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/helios-core/-/helios-core-0.1.0.tgz",
+      "integrity": "sha512-p2jmVSeciR9wcKLPc5seMxU0YsURF8ttLAIJS1CHU5fyoe40F3GXWPhsSdbGiDSRjLUcOzpjea1WTyGJ0zAgEA==",
       "requires": {
         "fs-extra": "^10.0.0",
         "got": "^11.8.3",
diff --git a/package.json b/package.json
index 547268c5..404004f4 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
     "fs-extra": "^10.0.0",
     "github-syntax-dark": "^0.5.0",
     "got": "^11.8.3",
-    "helios-core": "^0.1.0-alpha.5",
+    "helios-core": "~0.1.0",
     "jquery": "^3.6.0",
     "node-stream-zip": "^1.15.0",
     "request": "^2.88.2",