Skip to content
This repository has been archived by the owner on Aug 14, 2024. It is now read-only.

Commit

Permalink
i dont know
Browse files Browse the repository at this point in the history
  • Loading branch information
NanamiNakano committed Jan 12, 2024
1 parent d1276a9 commit 86f313e
Show file tree
Hide file tree
Showing 12 changed files with 197 additions and 42 deletions.
4 changes: 1 addition & 3 deletions lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-client-auth:$ktorVersion")
val log4j2Version = "2.20.0"
implementation("org.apache.logging.log4j:log4j-api:$log4j2Version")
implementation("org.apache.logging.log4j:log4j-core:$log4j2Version")
Expand All @@ -31,11 +32,8 @@ dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
api("org.apache.commons:commons-math3:3.6.1")
implementation("com.google.guava:guava:32.1.1-jre")
implementation(kotlin("reflect"))
implementation("com.microsoft.graph:microsoft-graph:5.77.0")
implementation("com.azure:azure-identity:1.11.1")
implementation("com.microsoft.azure:msal4j:1.14.2")
}

Expand Down
19 changes: 19 additions & 0 deletions lib/src/main/kotlin/mikataneko/interfaces/TokenCacheAspect.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package mikataneko.interfaces

import com.microsoft.aad.msal4j.ITokenCacheAccessAspect
import com.microsoft.aad.msal4j.ITokenCacheAccessContext

abstract class TokenCacheAspect : ITokenCacheAccessAspect {
override fun beforeCacheAccess(iTokenCacheAccessContext: ITokenCacheAccessContext?) {
val data = loadCacheData()
iTokenCacheAccessContext?.tokenCache()?.deserialize(data)
}

override fun afterCacheAccess(iTokenCacheAccessContext: ITokenCacheAccessContext?) {
val data = iTokenCacheAccessContext?.tokenCache()?.serialize()
data?.let { saveCacheData(it) }
}

abstract fun loadCacheData(): String
abstract fun saveCacheData(data: String)
}
16 changes: 16 additions & 0 deletions lib/src/main/kotlin/mikataneko/models/minecraft/GameItems.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package mikataneko.models.minecraft

import kotlinx.serialization.Serializable

@Serializable
data class GameItems(
val items: List<GameItem>,
val signature: String,
val keyId: String,
)

@Serializable
data class GameItem(
val name: String,
val signature: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package mikataneko.models.minecraft

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject


sealed class MinecraftAuthenticate

@Serializable
data class XboxMinecraftAuthenticate(
val identityToken: String,
) : MinecraftAuthenticate()

@Serializable
data class MinecraftAuthenticateResponse(
val username: String,
val roles: JsonElement,
val metadata: JsonObject,
@SerialName("access_token")
val accessToken: String,
@SerialName("token_type")
val tokenType: String,
@SerialName("expires_in")
val expiresIn: Int,
)
7 changes: 7 additions & 0 deletions lib/src/main/kotlin/mikataneko/models/xbox/DeviceCodeFlow.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package mikataneko.models.xbox

data class DeviceCodeFlow(
val clientId: String,
val tenantId: String = "consumers",
val scopes: List<String> = listOf("XboxLive.signin", "offline_access", "openid", "profile", "email"),
)
22 changes: 22 additions & 0 deletions lib/src/main/kotlin/mikataneko/models/xbox/XSTSAuthorize.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package mikataneko.models.xbox

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class XSTSAuthorize(
@SerialName("Properties")
val xstsProperties: XSTSProperties,
@SerialName("RelyingParty")
val relyingParty: String = "rp://api.minecraftservices.com/",
@SerialName("TokenType")
val tokenType: String = "JWT",
)

@Serializable
data class XSTSProperties(
@SerialName("UserTokens")
val userTokens: List<String>,
@SerialName("SandboxId")
val sandboxId: String = "RETAIL",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package mikataneko.models.xbox

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.Instant

sealed class XSTSAuthorizeResponse

@Serializable
data class XSTSError(
@SerialName("Identity")
val identity: String,
@SerialName("XErr")
val xErr: Int,
@SerialName("Message")
val message: String,
@SerialName("Redirect")
val redirect: String,
) : XSTSAuthorizeResponse()

@Serializable
data class XSTSSuccess(
@SerialName("IssueInstant")
@Serializable(with = InstantSerializer::class)
val issueInstant: Instant,
@SerialName("NotAfter")
@Serializable(with = InstantSerializer::class)
val notAfter: Instant,
@SerialName("Token")
val token: String,
@SerialName("DisplayClaims")
val displayClaims: DisplayClaims,
) : XSTSAuthorizeResponse()
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import kotlinx.serialization.Serializable
@Serializable
data class XboxAuthenticate(
@SerialName("Properties")
val properties: Properties,
val properties: XboxProperties,
@SerialName("RelyingParty")
val relyingParty: String = "http://auth.xboxlive.com",
@SerialName("TokenType")
val tokenType: String = "JWT",
)

@Serializable
data class Properties(
data class XboxProperties(
@SerialName("RpsTicket")
val rpsTicket: String,
@SerialName("AuthMethod")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ data class XboxAuthenticateResponse(

@Serializable
data class DisplayClaims(
val xui: List<Map<String, String>>,
val xui: List<XboxUserHash>,
)

@Serializable
data class XboxUserHash(
val uhs: String,
)

object InstantSerializer : KSerializer<Instant> {
Expand Down
11 changes: 5 additions & 6 deletions lib/src/main/kotlin/mikataneko/utils/Downloader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import kotlin.io.path.deleteExisting
import kotlin.text.toByteArray

class Downloader(
private val server: String = "https://resources.download.minecraft.net",
private val client: HttpClient,
private val server: String = "https://resources.download.minecraft.net",
) {
suspend fun getAndSaveVersionManifest(instance: GameInstance): MinecraftVersionManifest {
val path = instance.rootDirectory.versions.resolve(instance.id).resolve("${instance.id}.json")
Expand All @@ -41,7 +41,7 @@ class Downloader(
return minecraftVersion
}

suspend fun getAndSaveAssetIndex(manifest: MinecraftVersionManifest,rootDirectory: GameDirectory): AssetIndexes {
suspend fun getAndSaveAssetIndex(manifest: MinecraftVersionManifest, rootDirectory: GameDirectory): AssetIndexes {
val path = rootDirectory.assetIndexes.resolve("${manifest.assetIndex.id}.json")
val response = withContext(Dispatchers.IO) {
client.get(manifest.assetIndex.url)
Expand All @@ -58,7 +58,7 @@ class Downloader(
return assetIndexes
}

suspend fun getAndSaveObject(target: Object,rootDirectory: GameDirectory) {
suspend fun getAndSaveObject(target: Object, rootDirectory: GameDirectory) {
val resolveValue = target.hash.take(2) + "/" + target.hash
val url = Url(server).toURI().resolve(resolveValue).toString()
withContext(Dispatchers.IO) {
Expand All @@ -76,7 +76,7 @@ class Downloader(
}
}

suspend fun getAndSaveClientJar(manifest: MinecraftVersionManifest, id: String,rootDirectory: GameDirectory) {
suspend fun getAndSaveClientJar(manifest: MinecraftVersionManifest, id: String, rootDirectory: GameDirectory) {
val path = rootDirectory.versions.resolve(id).resolve("${id}.jar")
val target = manifest.downloads.client
withContext(Dispatchers.IO) {
Expand All @@ -92,7 +92,7 @@ class Downloader(
}
}

suspend fun getAndSaveLibrary(lib: ResolvedLibrary, id: String,rootDirectory: GameDirectory) {
suspend fun getAndSaveLibrary(lib: ResolvedLibrary, id: String, rootDirectory: GameDirectory) {
when (lib) {
is NativeLibrary -> {
withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -156,4 +156,3 @@ private suspend fun HttpClient.downloadFromUrl(url: String): Path {
tempFile
}
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package mikataneko.models
package mikataneko.utils

import com.microsoft.aad.msal4j.*
import io.ktor.client.*
Expand All @@ -7,17 +7,13 @@ import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.serialization.json.Json
import mikataneko.globalClient
import mikataneko.models.xbox.Properties
import mikataneko.models.xbox.XboxAuthenticate
import mikataneko.models.xbox.XboxAuthenticateResponse
import mikataneko.models.MicrosoftAuthenticatorException
import mikataneko.models.minecraft.GameItems
import mikataneko.models.minecraft.MinecraftAuthenticateResponse
import mikataneko.models.minecraft.XboxMinecraftAuthenticate
import mikataneko.models.xbox.*
import java.util.function.Consumer

data class DeviceCodeFlow(
val clientId: String,
val tenantId: String = "consumers",
val scopes: List<String> = listOf("XboxLive.signin", "offline_access", "openid", "profile", "email"),
)

class MicrosoftAuthenticator(
tokenCacheAspect: ITokenCacheAccessAspect,
private val deviceCodeFlow: DeviceCodeFlow,
Expand Down Expand Up @@ -69,27 +65,46 @@ class MicrosoftAuthenticator(
return result
}

suspend fun authenticateWithXboxLive(token: String): XboxAuthenticateResponse {
suspend fun authenticateWithXboxLive(accessToken: String): XboxAuthenticateResponse {
val response = client.post("https://user.auth.xboxlive.com/user/authenticate") {
contentType(ContentType.Application.Json)
setBody(XboxAuthenticate(Properties("d=$token")))
setBody(XboxAuthenticate(XboxProperties("d=$accessToken")))
accept(ContentType.Application.Json)
}
return Json.decodeFromString(response.bodyAsText())
}
}

abstract class TokenCacheAspect : ITokenCacheAccessAspect {
override fun beforeCacheAccess(iTokenCacheAccessContext: ITokenCacheAccessContext?) {
val data = loadCacheData()
iTokenCacheAccessContext?.tokenCache()?.deserialize(data)
suspend fun authorizeWithXSTS(xblToken: String): XSTSAuthorizeResponse {
val response = client.post("https://xsts.auth.xboxlive.com/xsts/authorize") {
contentType(ContentType.Application.Json)
setBody(XSTSAuthorize(XSTSProperties(listOf(xblToken))))
accept(ContentType.Application.Json)
}

if (response.status == HttpStatusCode.Unauthorized) {
return Json.decodeFromString<XSTSError>(response.bodyAsText())
}

return Json.decodeFromString<XSTSSuccess>(response.bodyAsText())
}

suspend fun authenticateWithMinecraft(userHash: String, xstsToken: String): MinecraftAuthenticateResponse {
val response = client.post("https://api.minecraftservices.com/authentication/login_with_xbox") {
contentType(ContentType.Application.Json)
setBody(XboxMinecraftAuthenticate("XBL3.0 x=$userHash;$xstsToken"))
accept(ContentType.Application.Json)
}

return Json.decodeFromString(response.bodyAsText())
}

override fun afterCacheAccess(iTokenCacheAccessContext: ITokenCacheAccessContext?) {
val data = iTokenCacheAccessContext?.tokenCache()?.serialize()
data?.let { saveCacheData(it) }
suspend fun getGameItems(minecraftAccessToken: String): GameItems {
val response = client.get("https://api.minecraftservices.com/entitlements/mcstore") {
header(HttpHeaders.Authorization, "Bearer $minecraftAccessToken")
}

return Json.decodeFromString(response.bodyAsText())
}

abstract fun loadCacheData(): String
abstract fun saveCacheData(data: String)

}
30 changes: 22 additions & 8 deletions lib/src/test/kotlin/mikataneko/TestMicrosoftAuth.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package mikataneko

import mikataneko.models.DeviceCodeFlow
import mikataneko.models.MicrosoftAuthenticator
import mikataneko.models.TokenCacheAspect
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import mikataneko.models.xbox.DeviceCodeFlow
import mikataneko.utils.MicrosoftAuthenticator
import mikataneko.interfaces.TokenCacheAspect
import mikataneko.models.xbox.XSTSError
import mikataneko.models.xbox.XSTSSuccess
import kotlin.io.path.readText
import kotlin.io.path.writeText

private val json = Json {
prettyPrint = true
}

suspend fun main() {
val deviceCodeFlow = DeviceCodeFlow(System.getenv("CLIENT_ID"))
val tokenCacheAspect = TokenCacheAspectImpl()
Expand All @@ -15,11 +23,17 @@ suspend fun main() {
println(deviceCode.message())
}

val token = result.accessToken()

val response = authenticator.authenticateWithXboxLive(token)

println(response.token)
val accessToken = result.accessToken()
val xblResponse = authenticator.authenticateWithXboxLive(accessToken)
val xblToken = xblResponse.token
val userHash = xblResponse.displayClaims.xui.first().uhs
val xstsToken = when (val xstsResponse = authenticator.authorizeWithXSTS(xblToken)) {
is XSTSError -> return
is XSTSSuccess -> xstsResponse.token
}
val minecraftToken = authenticator.authenticateWithMinecraft(userHash, xstsToken).accessToken
val productList = authenticator.getGameItems(minecraftToken)
println(json.encodeToString(productList))
}

class TokenCacheAspectImpl : TokenCacheAspect() {
Expand Down

0 comments on commit 86f313e

Please sign in to comment.