-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c35f58e
commit d4aeb2a
Showing
21 changed files
with
407 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
[*.cs] | ||
|
||
# IDE0005: Using 指令是不需要的。 | ||
dotnet_diagnostic.IDE0005.severity = none |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\MinecraftLaunch\MinecraftLaunch.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using MinecraftLaunch.Components.Authenticator; | ||
|
||
MicrosoftAuthenticator authenticator = new("9fd44410-8ed7-4eb3-a160-9f1cc62c824c"); | ||
|
||
var result = await authenticator.DeviceFlowAuthAsync(x => { | ||
Console.WriteLine(x.UserCode); | ||
}); | ||
|
||
if(result != null) { | ||
var account = await authenticator.AuthenticateAsync(); | ||
Console.WriteLine(account.Name); | ||
Console.WriteLine(account.Uuid); | ||
} | ||
|
||
Console.ReadKey(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
namespace MinecraftLaunch.Classes.Models.Auth { | ||
public record DeviceCodeResponse { | ||
[JsonPropertyName("user_code")] | ||
public string UserCode { get; set; } | ||
|
||
[JsonPropertyName("device_code")] | ||
public string DeviceCode { get; set; } | ||
|
||
[JsonPropertyName("verification_uri")] | ||
public string VerificationUrl { get; set; } | ||
|
||
[JsonPropertyName("expires_in")] | ||
public int ExpiresIn { get; set; } | ||
|
||
[JsonPropertyName("interval")] | ||
public int Interval { get; set; } | ||
|
||
[JsonPropertyName("message")] | ||
public string Message { get; set; } | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
MinecraftLaunch/Classes/Models/Auth/OAuth2TokenResponse.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Text.Json.Serialization; | ||
using System.Threading.Tasks; | ||
|
||
namespace MinecraftLaunch.Classes.Models.Auth { | ||
public record OAuth2TokenResponse { | ||
[JsonPropertyName("token_type")] | ||
public string TokenType { get; set; } | ||
|
||
[JsonPropertyName("expires_in")] | ||
public int ExpiresIn { get; set; } | ||
|
||
[JsonPropertyName("scope")] | ||
public string Scope { get; set; } | ||
|
||
[JsonPropertyName("access_token")] | ||
public string AccessToken { get; set; } | ||
|
||
[JsonPropertyName("refresh_token")] | ||
public string RefreshToken { get; set; } | ||
|
||
[JsonPropertyName("user_id")] | ||
public string UserId { get; set; } | ||
|
||
[JsonPropertyName("foci")] | ||
public string Foci { get; set; } | ||
} | ||
} |
203 changes: 198 additions & 5 deletions
203
MinecraftLaunch/Components/Authenticator/MicrosoftAuthenticator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,207 @@ | ||
using MinecraftLaunch.Classes.Interfaces; | ||
using Flurl.Http; | ||
using MinecraftLaunch.Classes.Enums; | ||
using MinecraftLaunch.Classes.Interfaces; | ||
using MinecraftLaunch.Classes.Models.Auth; | ||
using MinecraftLaunch.Extensions; | ||
using System.Reflection.Metadata; | ||
using System.Text.Json; | ||
using System.Text.Json.Nodes; | ||
using static System.Formats.Asn1.AsnWriter; | ||
|
||
namespace MinecraftLaunch.Components.Authenticator {//W.I.P | ||
public class MicrosoftAuthenticator : IAuthenticator<MicrosoftAccount> { | ||
public class MicrosoftAuthenticator(string clientId) : IAuthenticator<MicrosoftAccount> { | ||
private MicrosoftAccount _account; | ||
|
||
private string _clientId = clientId; | ||
|
||
private OAuth2TokenResponse _oAuth2TokenResponse; | ||
|
||
private IEnumerable<string> _scopes => ["XboxLive.signin", "offline_access", "openid", "profile", "email"]; | ||
|
||
public bool IsCheckOwnership { get; set; } | ||
|
||
public MicrosoftAuthenticator(MicrosoftAccount account) : this(string.Empty) { | ||
_account = account; | ||
} | ||
|
||
public MicrosoftAccount Authenticate() { | ||
return AuthenticateAsync().GetAwaiter().GetResult(); | ||
var task = AuthenticateAsync(); | ||
if (task.IsCompleted) { | ||
return task.GetAwaiter().GetResult(); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public async ValueTask<MicrosoftAccount> AuthenticateAsync() { | ||
/* | ||
* Refresh token | ||
*/ | ||
if (_account is not null) { | ||
var url = "https://login.live.com/oauth20_token.srf"; | ||
|
||
var content = new { | ||
client_id = _clientId, | ||
refresh_token = _account.RefreshToken, | ||
grant_type = "refresh_token", | ||
}; | ||
|
||
var result = await url.PostUrlEncodedAsync(content); | ||
_oAuth2TokenResponse = await result.GetJsonAsync<OAuth2TokenResponse>(); | ||
} | ||
|
||
/* | ||
* Get Xbox live token | ||
*/ | ||
var xblContent = new { | ||
Properties = new { | ||
AuthMethod = "RPS", | ||
SiteName = "user.auth.xboxlive.com", | ||
RpsTicket = $"d={_oAuth2TokenResponse.AccessToken}" | ||
}, | ||
RelyingParty = "http://auth.xboxlive.com", | ||
TokenType = "JWT" | ||
}; | ||
|
||
using var xblJsonReq = await $"https://user.auth.xboxlive.com/user/authenticate" | ||
.PostJsonAsync(xblContent); | ||
|
||
var xblTokenNode = JsonNode.Parse(await xblJsonReq.GetStringAsync()); | ||
|
||
/* | ||
* Get Xbox security token service token | ||
*/ | ||
var xstsContent = new { | ||
Properties = new { | ||
SandboxId = "RETAIL", | ||
UserTokens = new[] { | ||
xblTokenNode.GetString("Token") | ||
} | ||
}, | ||
RelyingParty = "rp://api.minecraftservices.com/", | ||
TokenType = "JWT" | ||
}; | ||
|
||
using var xstsJsonReq = await $"https://xsts.auth.xboxlive.com/xsts/authorize" | ||
.PostJsonAsync(xstsContent); | ||
|
||
var xstsTokenNode = JsonNode.Parse(await xstsJsonReq.GetStringAsync()); | ||
|
||
/* | ||
* Authenticate minecraft account | ||
*/ | ||
var authenticateMinecraftContent = new { | ||
identityToken = $"XBL3.0 x={xblTokenNode["DisplayClaims"]["xui"].AsArray() | ||
.FirstOrDefault() | ||
.GetString("uhs")};{xstsTokenNode! | ||
.GetString("Token")}" | ||
}; | ||
|
||
using var authenticateMinecraftPostRes = await $"https://api.minecraftservices.com/authentication/login_with_xbox" | ||
.PostJsonAsync(authenticateMinecraftContent); | ||
|
||
string access_token = JsonNode.Parse(await authenticateMinecraftPostRes.GetStringAsync())!["access_token"]! | ||
.GetValue<string>(); | ||
|
||
/* | ||
* Check player's minecraft ownership (optional steps) | ||
*/ | ||
if (IsCheckOwnership) { | ||
using var gameHasRes = await "https://api.minecraftservices.com/entitlements/mcstore" | ||
.WithHeader("Authorization", $"Bearer {access_token}") | ||
.GetAsync(); | ||
|
||
var ownNode = JsonNode.Parse(await gameHasRes.GetStringAsync()); | ||
if (!ownNode["items"].AsArray().Any()) { | ||
throw new OperationCanceledException("Game not purchased, login terminated"); | ||
} | ||
} | ||
|
||
/* | ||
* Get player's minecraft profile | ||
*/ | ||
using var profileRes = await "https://api.minecraftservices.com/minecraft/profile" | ||
.WithHeader("Authorization", $"Bearer {access_token}") | ||
.GetAsync(); | ||
|
||
var profileNode = JsonNode.Parse(await profileRes.GetStringAsync()); | ||
string refreshToken = _oAuth2TokenResponse is null && string | ||
.IsNullOrEmpty(_oAuth2TokenResponse.RefreshToken) | ||
? "None" | ||
: _oAuth2TokenResponse.RefreshToken; | ||
|
||
return new MicrosoftAccount { | ||
AccessToken = access_token, | ||
Type = AccountType.Microsoft, | ||
Name = profileNode.GetString("name"), | ||
Uuid = Guid.Parse(profileNode.GetString("id")), | ||
RefreshToken = refreshToken | ||
}; | ||
} | ||
|
||
public ValueTask<MicrosoftAccount> AuthenticateAsync() { | ||
throw new NotImplementedException(); | ||
public async Task<OAuth2TokenResponse> DeviceFlowAuthAsync(Action<DeviceCodeResponse> deviceCode, | ||
CancellationTokenSource source = default) { | ||
if (string.IsNullOrEmpty(_clientId)) { | ||
throw new ArgumentNullException("ClientId is empty!"); | ||
} | ||
|
||
var token = source?.Token; | ||
string tenant = "/consumers"; | ||
var parameters = new Dictionary<string, string> { | ||
["client_id"] = _clientId, | ||
["tenant"] = tenant, | ||
["scope"] = string.Join(" ", _scopes) | ||
}; | ||
|
||
string json = await "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode" | ||
.PostUrlEncodedAsync(parameters) | ||
.ReceiveString(); | ||
|
||
var codeResponse = JsonSerializer.Deserialize<DeviceCodeResponse>(json); | ||
deviceCode.Invoke(codeResponse); | ||
|
||
//Polling | ||
TimeSpan pollingInterval = TimeSpan.FromSeconds(codeResponse.Interval); | ||
DateTimeOffset codeExpiresOn = DateTimeOffset.UtcNow.AddSeconds(codeResponse.ExpiresIn); | ||
TimeSpan timeRemaining = codeExpiresOn - DateTimeOffset.UtcNow; | ||
OAuth2TokenResponse tokenResponse = default!; | ||
|
||
while (timeRemaining.TotalSeconds > 0) { | ||
if (token.HasValue && token.Value.IsCancellationRequested) { | ||
break; | ||
} | ||
|
||
parameters = new Dictionary<string, string> { | ||
["grant_type"] = "urn:ietf:params:oauth:grant-type:device_code", | ||
["device_code"] = codeResponse.DeviceCode, | ||
["client_id"] = _clientId, | ||
["tenant"] = tenant | ||
}; | ||
|
||
string tokenJson = await "https://login.microsoftonline.com/consumers/oauth2/v2.0/token" | ||
.PostUrlEncodedAsync(new FormUrlEncodedContent(parameters)) | ||
.ReceiveString(); | ||
|
||
var tempTokenResponse = JsonNode.Parse(tokenJson); | ||
|
||
if (tempTokenResponse["error"] == null) { | ||
tokenResponse = new() { | ||
AccessToken = tempTokenResponse.GetString("access_token"), | ||
RefreshToken = tempTokenResponse.GetString("refresh_token"), | ||
ExpiresIn = tempTokenResponse.GetInt32("expires_in"), | ||
}; | ||
} | ||
|
||
if (tempTokenResponse["token_type"]?.GetValue<string>() is "Bearer") { | ||
_oAuth2TokenResponse = tokenResponse; | ||
return tokenResponse; | ||
} | ||
|
||
await Task.Delay(pollingInterval); | ||
timeRemaining = codeExpiresOn - DateTimeOffset.UtcNow; | ||
} | ||
|
||
throw new TimeoutException("登录操作已超时"); | ||
} | ||
} | ||
} |
Oops, something went wrong.