Difference between revisions of "Microsoft Authentication Scheme"

From wiki.vg
Jump to navigation Jump to search
(IDK what to add here)
 
(→‎Authenticate with XBL: t=<access_token> not working now, d=<access_token> works)
 
(33 intermediate revisions by 14 users not shown)
Line 1: Line 1:
 
Minecraft is moving to Microsoft accounts. Starting December 2020, all new Accounts already use the new system, old accounts will be migrated later, see [https://www.minecraft.net/en-us/article/java-edition-moving-house this blog post]
 
Minecraft is moving to Microsoft accounts. Starting December 2020, all new Accounts already use the new system, old accounts will be migrated later, see [https://www.minecraft.net/en-us/article/java-edition-moving-house this blog post]
  
There are multiple steps and different token required, but in the end, you get a normal minecraft token back. Launching the game itself hasn't changed.
+
There are multiple steps and different tokens required, but in the end, you get a normal Minecraft token back. Launching the game itself hasn't changed.
  
 +
== Microsoft OAuth Flow ==
 +
[[File:minecraft_login.png|frame|Example of the login page]]
  
== Microsoft OAuth Flow ==
+
Prior to any of these steps, you will first need to obtain an [https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow OAuth 2.0] Client ID & secret by creating a [https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app  Microsoft Azure application].
 +
 
 +
In the first step, we are logging into the Microsoft account. This has to be done in a browser/webview!
  
In the first step, we are logging into the microsoft account. This has to be done in a browser/webview! Other redirect urls have not been tested. The client id is hardcoded, it's Minecrafts id.
+
The URL generated, either manually, or through an OAuth2 library, would look something like this:
  
 
  https://login.live.com/oauth20_authorize.srf
 
  https://login.live.com/oauth20_authorize.srf
   ?client_id=00000000402b5328
+
   ?client_id=<your Azure client ID>
 
   &response_type=code
 
   &response_type=code
   &scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL
+
  &redirect_uri=<your redirect uri>
   &redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf
+
   &scope=XboxLive.signin%20offline_access //without offline_access you won't get an refresh_token
 +
   &state=<optional; used to prevent CSRF & restoring previous application states>
  
Example of the login page: https://i.imgur.com/gy8uKGs.png (TODO: embed image)
+
<i>Note: You may also use the Azure Active Directory endpoints which would look like https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize but this just redirects to the live.com URL.</i>
  
The user will be prompted to enter username (E-Mail, Skype ID, Phone number, whatever) and his password. If those are legal, the user will be redirected. The user doesn't need to own MC, that check comes way later!
+
The user will be prompted to enter a username (E-Mail, Skype ID, Phone number, whatever) and their password. If those are valid, the user will be redirected. The user doesn't need to own MC, that check comes way later!
  
The redirect will looks something like this
+
The redirect will look something like this
  https://login.live.com/oauth20_desktop.srf?code=codegoeshere&lc=1033
+
  https://<your redirect URL>?code=codegoeshere&state=<optional; only if provided>
  
 
You have to extract the code param, it's your Microsoft Authorization Code.
 
You have to extract the code param, it's your Microsoft Authorization Code.
Line 25: Line 30:
 
== Authorization Code -> Authorization Token ==
 
== Authorization Code -> Authorization Token ==
  
The next step is to get a auth token from the auth code. This isn't done in the browser for security reasons.
+
The next step is to get an access token from the auth code. This isn't done in the browser for security reasons.
 +
 
 +
<i>This exchange should be done on the server-side as it requires the use of your client secret which, as the name implies, should be kept secret.</i>
 +
 
 
  POST https://login.live.com/oauth20_token.srf
 
  POST https://login.live.com/oauth20_token.srf
Content:
+
  client_id=<your client id>
Map<Object, Object> data = Map.of(
+
  &client_secret=<your client secret>
    "client_id", "00000000402b5328", // minecrafts client id again
+
  &code=<auth code / the code from step 1>
    "code", authcode, // the code from step 1
+
  &grant_type=authorization_code
    "grant_type", "authorization_code",
+
  &redirect_uri=<your redirect uri>
    "redirect_uri", "https://login.live.com/oauth20_desktop.srf",
 
    "scope", "service::user.auth.xboxlive.com::MBI_SSL"
 
);
 
  
 
Don't forget to set <code>Content-Type: application/x-www-form-urlencoded</code>
 
Don't forget to set <code>Content-Type: application/x-www-form-urlencoded</code>
  
The response will look like this
+
The response will look like this:
 +
 
 +
<syntaxhighlight lang="json" line='line'>
 
  {
 
  {
 
   "token_type":"bearer",
 
   "token_type":"bearer",
 
   "expires_in":86400,
 
   "expires_in":86400,
   "scope":"service::user.auth.xboxlive.com::MBI_SSL",
+
   "scope":"XboxLive.signin",
 
   "access_token":"token here",
 
   "access_token":"token here",
 
   "refresh_token":"M.R3_BAY.token here",
 
   "refresh_token":"M.R3_BAY.token here",
Line 48: Line 55:
 
   "foci":"1"
 
   "foci":"1"
 
  }
 
  }
 +
</syntaxhighlight>
  
We care about the access_token here. (TODO: check what we can do with the refresh token)
+
We care about the access_token here.  
 +
 
 +
== Refreshing Tokens ==
 +
You can use the refresh_token you got in the previous request to acquire a new access_token. Just call the same endpoint as before, switching out code for the refresh token and grant type <code>authorization_code</code> for <code>refresh_token</code>
 +
 
 +
POST https://login.live.com/oauth20_token.srf
 +
  client_id=<your client id>
 +
  &client_secret=<your client secret>
 +
  &refresh_token=<refresh token from previous step>
 +
  &grant_type=refresh_token
 +
  &redirect_uri=<your redirect uri>
 +
 
 +
(the response is the same as for requesting tokens with a code)
  
 
== Authenticate with XBL ==
 
== Authenticate with XBL ==
  
Now that we are authenticated with microsoft, we can authenticate to xbox live.
+
Now that we are authenticated with Microsoft, we can authenticate to Xbox Live.
  
 
To do that, we send
 
To do that, we send
 +
 +
<syntaxhighlight lang="json" line='line'>
 
  POST https://user.auth.xboxlive.com/user/authenticate
 
  POST https://user.auth.xboxlive.com/user/authenticate
 
  {
 
  {
Line 61: Line 83:
 
         "AuthMethod": "RPS",
 
         "AuthMethod": "RPS",
 
         "SiteName": "user.auth.xboxlive.com",
 
         "SiteName": "user.auth.xboxlive.com",
         "RpsTicket": "access_token" // your access token from step 2 here
+
         "RpsTicket": "d=<access_token>" // your access token from step 2 here
 
     },
 
     },
 
     "RelyingParty": "http://auth.xboxlive.com",
 
     "RelyingParty": "http://auth.xboxlive.com",
 
     "TokenType": "JWT"
 
     "TokenType": "JWT"
 
  }
 
  }
 +
</syntaxhighlight>
  
 
Again, it will complain if you don't set <code>Content-Type: application/json</code> and <code>Accept: application/json</code>
 
Again, it will complain if you don't set <code>Content-Type: application/json</code> and <code>Accept: application/json</code>
  
 
The response will look like this:
 
The response will look like this:
 +
 +
<syntaxhighlight lang="json" line='line'>
 
  {
 
  {
 
   "IssueInstant":"2020-12-07T19:52:08.4463796Z",
 
   "IssueInstant":"2020-12-07T19:52:08.4463796Z",
 
   "NotAfter":"2020-12-21T19:52:08.4463796Z",
 
   "NotAfter":"2020-12-21T19:52:08.4463796Z",
   "Token":"token", // save this
+
   "Token":"token", // save this, this is your xbl token
 
   "DisplayClaims":{
 
   "DisplayClaims":{
 
       "xui":[
 
       "xui":[
 
         {
 
         {
             "uhs":"uhs" // save this
+
             "uhs":"userhash" // save this
 
         }
 
         }
 
       ]
 
       ]
 
   }
 
   }
 
  }
 
  }
 +
</syntaxhighlight>
  
We need to save token and uhs. I have no idea what uhs stands for. (TODO: find out)
+
We need to save token and userhash.
  
 
== Authenticate with XSTS ==
 
== Authenticate with XSTS ==
  
Now that we are authenticated with XBL, we need to get a XSTS token, we can use to login to minecraft.
+
Now that we are authenticated with XBL, we need to get a XSTS token, we can use to login to Minecraft.
  
 +
<syntaxhighlight lang="json" line='line'>
 
  POST https://xsts.auth.xboxlive.com/xsts/authorize
 
  POST https://xsts.auth.xboxlive.com/xsts/authorize
 +
{
 +
    "Properties": {
 +
        "SandboxId": "RETAIL",
 +
        "UserTokens": [
 +
            "xbl_token" // from above
 +
        ]
 +
    },
 +
    "RelyingParty": "rp://api.minecraftservices.com/",
 +
    "TokenType": "JWT"
 +
}
 +
</syntaxhighlight>
 +
 +
Again, set content type and accept to json.
 +
 +
Response will look like this:
 +
 +
<syntaxhighlight lang="json" line='line'>
 +
{
 +
  "IssueInstant":"2020-12-07T19:52:09.2345095Z",
 +
  "NotAfter":"2020-12-08T11:52:09.2345095Z",
 +
  "Token":"token", // save this, this is your xsts token
 +
  "DisplayClaims":{
 +
      "xui":[
 +
        {
 +
            "uhs":"userhash" // same as last request
 +
        }
 +
      ]
 +
  }
 +
}
 +
</syntaxhighlight>
 +
 +
The endpoint can return a 401 error with the below response:
 +
 +
<syntaxhighlight lang="json" line='line'>
 +
{
 +
    "Identity":"0",
 +
    "XErr":2148916238,
 +
    "Message":"",
 +
    "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily"
 +
}
 +
</syntaxhighlight>
 +
 +
The Redirect parameter usually will not resolve or go anywhere in a browser, likely they're targeting Xbox consoles.
 +
 +
Noted XErr codes and their meanings:
 +
 +
* '''2148916233''': The account doesn't have an Xbox account. Once they sign up for one (or login through minecraft.net to create one) then they can proceed with the login. This shouldn't happen with accounts that have purchased Minecraft with a Microsoft account, as they would've already gone through that Xbox signup process.
 +
* '''2148916238''': The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult. This only seems to occur when using a custom Microsoft Azure application. When using the Minecraft launchers client id, this doesn't trigger.
 +
 +
== Authenticate with Minecraft ==
 +
 +
Now we can finally start talking to Minecraft. The XSTS token from the last request allows us to authenticate to Minecraft using
 +
 +
<syntaxhighlight lang="json" line='line'>
 +
POST https://api.minecraftservices.com/authentication/login_with_xbox
 +
{
 +
    "identityToken": "XBL3.0 x=<userhash>;<xsts_token>"
 +
}
 +
</syntaxhighlight>
 +
 +
Response:
 +
<syntaxhighlight lang="json" line='line'>
 +
{
 +
  "username" : "some uuid", // this is not the uuid of the account
 +
  "roles" : [ ],
 +
  "access_token" : "minecraft access token", // jwt, your good old minecraft access token
 +
  "token_type" : "Bearer",
 +
  "expires_in" : 86400
 +
}
 +
</syntaxhighlight>
 +
 +
This access token allows us to launch the game, but, we haven't actually checked if the account owns the game. Everything until here works with a normal Microsoft account!
 +
 +
== Checking Game Ownership ==
 +
 +
So let's use our mc access token to check if a product licence is attached to the account.
 +
GET https://api.minecraftservices.com/entitlements/mcstore
 +
 +
The access token goes into the auth header: <code>Authorization: Bearer <Minecraft Access Token></code>. (Keep in mind that <code>Bearer </code> is actually the prefix you must include!)
 +
 +
If the account owns the game, the response will look like this:
 +
 +
<syntaxhighlight lang="json" line='line'>
 +
{
 +
  "items" : [ {
 +
    "name" : "product_minecraft",
 +
    "signature" : "jwt sig"
 +
  }, {
 +
    "name" : "game_minecraft",
 +
    "signature" : "jwt sig"
 +
  } ],
 +
  "signature" : "jwt sig",
 +
  "keyId" : "1"
 +
}
 +
</syntaxhighlight>
 +
 +
The first jwts contain the values:
 +
<syntaxhighlight lang="json" line='line'>
 +
{
 +
  "typ": "JWT",
 +
  "alg": "RS256",
 +
  "kid": "1"
 +
}.{
 +
  "signerId": "2535416586892404",
 +
  "name": "product_minecraft"
 +
}.[Signature]
 +
</syntaxhighlight>
 +
 +
the last jwt looks like this decoded:
 +
 +
<syntaxhighlight lang="json" line='line'>
 +
{
 +
  "typ": "JWT",
 +
  "alg": "RS256",
 +
  "kid": "1"
 +
}.{
 +
  "entitlements": [
 +
    {
 +
      "name": "product_minecraft"
 +
    },
 +
    {
 +
      "name": "game_minecraft"
 +
    }
 +
  ],
 +
  "signerId": "2535416586892404"
 +
}.[Signature]
 +
</syntaxhighlight>
 +
 +
If the account doesn't own the game, the items array will be empty.
 +
 +
== Get the profile ==
 +
 +
Now that we know that the account owns the game, lets get his profile so we get uuid:
 +
GET https://api.minecraftservices.com/minecraft/profile
 +
 +
Again, the access token goes into the auth header: <code>Authorization: Bearer token</code>
 +
 +
The response will look like this, if the account owns the game:
 +
 +
<syntaxhighlight lang="json" line='line'>
 +
{
 +
  "id" : "986dec87b7ec47ff89ff033fdb95c4b5", // the real uuid of the account, woo
 +
  "name" : "HowDoesAuthWork", // the mc user name of the account
 +
  "skins" : [ {
 +
    "id" : "6a6e65e5-76dd-4c3c-a625-162924514568",
 +
    "state" : "ACTIVE",
 +
    "url" : "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b",
 +
    "variant" : "CLASSIC",
 +
    "alias" : "STEVE"
 +
  } ],
 +
  "capes" : [ ]
 +
}
 +
</syntaxhighlight>
 +
 +
Else it will look like this:
 +
 +
<syntaxhighlight lang="json" line='line'>
 +
{
 +
  "path" : "/minecraft/profile",
 +
  "errorType" : "NOT_FOUND",
 +
  "error" : "NOT_FOUND",
 +
  "errorMessage" : "The server has not found anything matching the request URI",
 +
  "developerMessage" : "The server has not found anything matching the request URI"
 +
}
 +
</syntaxhighlight>
 +
 +
You should know have all necessary data (the mc access token, the username and the uuid) to launch the game. Well done!
 +
 +
== Sample Implementations ==
 +
 +
There is a rough sample implementation in Java (using javafx and its webview) [https://github.com/MiniDigger/MiniLauncher/blob/master/launcher/src/main/java/me/minidigger/minecraftlauncher/launcher/gui/MsaFragmentController.java here], and in Go [https://gist.github.com/rbrick/be8ed86864fc5d77aa6c979053cfc892 here].
 +
[https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/src/client/microsoftAuth.js Js implementation]
 +
 +
A implementation in Kotlin (with JavaFX) can be found here: [https://gitlab.bixilon.de/bixilon/minosoft/-/blob/development/src/main/java/de/bixilon/minosoft/gui/main/dialogs/login/MicrosoftLoginController.java here] and [https://gitlab.bixilon.de/bixilon/minosoft/-/blob/development/src/main/java/de/bixilon/minosoft/util/microsoft/MicrosoftOAuthUtils.kt here]

Latest revision as of 18:09, 19 April 2021

Minecraft is moving to Microsoft accounts. Starting December 2020, all new Accounts already use the new system, old accounts will be migrated later, see this blog post

There are multiple steps and different tokens required, but in the end, you get a normal Minecraft token back. Launching the game itself hasn't changed.

Microsoft OAuth Flow

Example of the login page

Prior to any of these steps, you will first need to obtain an OAuth 2.0 Client ID & secret by creating a Microsoft Azure application.

In the first step, we are logging into the Microsoft account. This has to be done in a browser/webview!

The URL generated, either manually, or through an OAuth2 library, would look something like this:

https://login.live.com/oauth20_authorize.srf
 ?client_id=<your Azure client ID>
 &response_type=code
 &redirect_uri=<your redirect uri>
 &scope=XboxLive.signin%20offline_access //without offline_access you won't get an refresh_token
 &state=<optional; used to prevent CSRF & restoring previous application states>

Note: You may also use the Azure Active Directory endpoints which would look like https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize but this just redirects to the live.com URL.

The user will be prompted to enter a username (E-Mail, Skype ID, Phone number, whatever) and their password. If those are valid, the user will be redirected. The user doesn't need to own MC, that check comes way later!

The redirect will look something like this

https://<your redirect URL>?code=codegoeshere&state=<optional; only if provided>

You have to extract the code param, it's your Microsoft Authorization Code.

Authorization Code -> Authorization Token

The next step is to get an access token from the auth code. This isn't done in the browser for security reasons.

This exchange should be done on the server-side as it requires the use of your client secret which, as the name implies, should be kept secret.

POST https://login.live.com/oauth20_token.srf
 client_id=<your client id>
 &client_secret=<your client secret>
 &code=<auth code / the code from step 1>
 &grant_type=authorization_code
 &redirect_uri=<your redirect uri>

Don't forget to set Content-Type: application/x-www-form-urlencoded

The response will look like this:

1  {
2    "token_type":"bearer",
3    "expires_in":86400,
4    "scope":"XboxLive.signin",
5    "access_token":"token here",
6    "refresh_token":"M.R3_BAY.token here",
7    "user_id":"889ed4a3d844f672",
8    "foci":"1"
9  }

We care about the access_token here.

Refreshing Tokens

You can use the refresh_token you got in the previous request to acquire a new access_token. Just call the same endpoint as before, switching out code for the refresh token and grant type authorization_code for refresh_token

POST https://login.live.com/oauth20_token.srf
 client_id=<your client id>
 &client_secret=<your client secret>
 &refresh_token=<refresh token from previous step>
 &grant_type=refresh_token
 &redirect_uri=<your redirect uri>

(the response is the same as for requesting tokens with a code)

Authenticate with XBL

Now that we are authenticated with Microsoft, we can authenticate to Xbox Live.

To do that, we send

 1  POST https://user.auth.xboxlive.com/user/authenticate
 2  {
 3     "Properties": {
 4         "AuthMethod": "RPS",
 5         "SiteName": "user.auth.xboxlive.com",
 6         "RpsTicket": "d=<access_token>" // your access token from step 2 here
 7     },
 8     "RelyingParty": "http://auth.xboxlive.com",
 9     "TokenType": "JWT"
10  }

Again, it will complain if you don't set Content-Type: application/json and Accept: application/json

The response will look like this:

 1  {
 2    "IssueInstant":"2020-12-07T19:52:08.4463796Z",
 3    "NotAfter":"2020-12-21T19:52:08.4463796Z",
 4    "Token":"token", // save this, this is your xbl token
 5    "DisplayClaims":{
 6       "xui":[
 7          {
 8             "uhs":"userhash" // save this
 9          }
10       ]
11    }
12  }

We need to save token and userhash.

Authenticate with XSTS

Now that we are authenticated with XBL, we need to get a XSTS token, we can use to login to Minecraft.

 1  POST https://xsts.auth.xboxlive.com/xsts/authorize
 2  {
 3     "Properties": {
 4         "SandboxId": "RETAIL",
 5         "UserTokens": [
 6             "xbl_token" // from above
 7         ]
 8     },
 9     "RelyingParty": "rp://api.minecraftservices.com/",
10     "TokenType": "JWT"
11  }

Again, set content type and accept to json.

Response will look like this:

 1  {
 2    "IssueInstant":"2020-12-07T19:52:09.2345095Z",
 3    "NotAfter":"2020-12-08T11:52:09.2345095Z",
 4    "Token":"token", // save this, this is your xsts token
 5    "DisplayClaims":{
 6       "xui":[
 7          {
 8             "uhs":"userhash" // same as last request
 9          }
10       ]
11    }
12 }

The endpoint can return a 401 error with the below response:

1  {
2     "Identity":"0",
3     "XErr":2148916238,
4     "Message":"",
5     "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily"
6  }

The Redirect parameter usually will not resolve or go anywhere in a browser, likely they're targeting Xbox consoles.

Noted XErr codes and their meanings:

  • 2148916233: The account doesn't have an Xbox account. Once they sign up for one (or login through minecraft.net to create one) then they can proceed with the login. This shouldn't happen with accounts that have purchased Minecraft with a Microsoft account, as they would've already gone through that Xbox signup process.
  • 2148916238: The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult. This only seems to occur when using a custom Microsoft Azure application. When using the Minecraft launchers client id, this doesn't trigger.

Authenticate with Minecraft

Now we can finally start talking to Minecraft. The XSTS token from the last request allows us to authenticate to Minecraft using

1  POST https://api.minecraftservices.com/authentication/login_with_xbox
2  {
3     "identityToken": "XBL3.0 x=<userhash>;<xsts_token>"
4  }

Response:

1  {
2   "username" : "some uuid", // this is not the uuid of the account
3   "roles" : [ ],
4   "access_token" : "minecraft access token", // jwt, your good old minecraft access token
5   "token_type" : "Bearer",
6   "expires_in" : 86400
7  }

This access token allows us to launch the game, but, we haven't actually checked if the account owns the game. Everything until here works with a normal Microsoft account!

Checking Game Ownership

So let's use our mc access token to check if a product licence is attached to the account.

GET https://api.minecraftservices.com/entitlements/mcstore

The access token goes into the auth header: Authorization: Bearer <Minecraft Access Token>. (Keep in mind that Bearer is actually the prefix you must include!)

If the account owns the game, the response will look like this:

 1  {
 2   "items" : [ {
 3     "name" : "product_minecraft",
 4     "signature" : "jwt sig"
 5   }, {
 6     "name" : "game_minecraft",
 7     "signature" : "jwt sig"
 8   } ],
 9   "signature" : "jwt sig",
10   "keyId" : "1"
11  }

The first jwts contain the values:

1  {
2   "typ": "JWT",
3   "alg": "RS256",
4   "kid": "1"
5  }.{
6   "signerId": "2535416586892404",
7   "name": "product_minecraft"
8  }.[Signature]

the last jwt looks like this decoded:

 1  {
 2   "typ": "JWT",
 3   "alg": "RS256",
 4   "kid": "1"
 5  }.{
 6   "entitlements": [
 7     {
 8       "name": "product_minecraft"
 9     },
10     {
11       "name": "game_minecraft"
12     }
13   ],
14   "signerId": "2535416586892404"
15  }.[Signature]

If the account doesn't own the game, the items array will be empty.

Get the profile

Now that we know that the account owns the game, lets get his profile so we get uuid:

GET https://api.minecraftservices.com/minecraft/profile

Again, the access token goes into the auth header: Authorization: Bearer token

The response will look like this, if the account owns the game:

 1  {
 2   "id" : "986dec87b7ec47ff89ff033fdb95c4b5", // the real uuid of the account, woo
 3   "name" : "HowDoesAuthWork", // the mc user name of the account
 4   "skins" : [ {
 5     "id" : "6a6e65e5-76dd-4c3c-a625-162924514568",
 6     "state" : "ACTIVE",
 7     "url" : "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b",
 8     "variant" : "CLASSIC",
 9     "alias" : "STEVE"
10   } ],
11   "capes" : [ ]
12  }

Else it will look like this:

1  {
2   "path" : "/minecraft/profile",
3   "errorType" : "NOT_FOUND",
4   "error" : "NOT_FOUND",
5   "errorMessage" : "The server has not found anything matching the request URI",
6   "developerMessage" : "The server has not found anything matching the request URI"
7  }

You should know have all necessary data (the mc access token, the username and the uuid) to launch the game. Well done!

Sample Implementations

There is a rough sample implementation in Java (using javafx and its webview) here, and in Go here. Js implementation

A implementation in Kotlin (with JavaFX) can be found here: here and here