Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a1a9100
我们孤身闯入这世界里 找寻名为宝藏的通关奖励
copytiao Feb 15, 2026
f10b822
越过深渊里迷人的金币 最后发现 珍贵是我和你
copytiao Feb 15, 2026
aa9fd4e
终于你可以说起那些心碎的过去 不过是 一场 狂风暴雨
copytiao Feb 16, 2026
59c5ffa
传说中偏爱少年勇敢举起的手臂 我爱你 笨拙 的心
copytiao Feb 16, 2026
81ebe6a
从蹒跚 到奔袭 从沙漠 到荆棘 你已赤脚穿过 这片陆地
copytiao Feb 16, 2026
44a5847
习惯了 空欢喜 学会了不哭泣 每颗珍珠都曾是 痛过的沙粒
copytiao Feb 16, 2026
a4b9a31
我在等你 找到你 一直到太阳升起 多少次坠下谷底 也能抱住自己
copytiao Feb 16, 2026
aa49183
山上的风 地心的力 生命向上长成了自己 那时你会看到春野满地
copytiao Feb 16, 2026
ecf2387
从失眠 到失意 从失落 到失去 多少痛的潮汐 曾吻过你
copytiao Feb 16, 2026
1b5b9a9
推开门 走出去 世界也在等着你 你是珍珠要亲手捧出你自己
copytiao Feb 21, 2026
3e0ab3f
此刻的你 找到你 我看见大雾散去 感谢你无数次拼命地拉住了自己
copytiao Feb 21, 2026
e46c27d
肩上的风 和手心的力 会化作你掌纹的痕迹 每个日出拥抱崭新的你
copytiao Feb 21, 2026
b3bfeb6
我要陪你 走下去 一直到无边天际 感谢我们无数次交付彼此的勇气
copytiao Feb 21, 2026
1f4a815
好的天气 坏的运气 都是值得庆祝的相遇 当我们开始真的爱自己
copytiao Feb 21, 2026
0e15c9d
传说的宝藏就是你
copytiao Feb 21, 2026
53917bf
Tun on the light 跟上这节拍
copytiao Feb 21, 2026
d446d78
Dance all right 梦驱散阴霾
copytiao Feb 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.IdentityModel.Tokens;
using System.Text.Json.Serialization;

namespace PCL.Core.Minecraft.IdentityModel.Extensions.JsonWebToken;

public record JsonWebKeys
{
[JsonPropertyName("keys")] public required JsonWebKey[] Keys;
}
57 changes: 57 additions & 0 deletions PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Documents;
using PCL.Core.Minecraft.IdentityModel.Extensions.Pkce;
using PCL.Core.Minecraft.IdentityModel.OAuth;
using PCL.Core.Utils.Exts;

namespace PCL.Core.Minecraft.IdentityModel.Extensions.OpenId;

public class OpenIdClient(OpenIdOptions options):IOAuthClient
{
private IOAuthClient? _client;

public async Task InitialAsync(CancellationToken token,bool checkAddress = false)
{
var opt = await options.BuildOAuthOptionsAsync(token);
if (!checkAddress || opt.Meta.AuthorizeEndpoint.IsNullOrEmpty() || opt.Meta.DeviceEndpoint.IsNullOrEmpty())
{
_client = options.EnablePkceSupport ? new PkceClient(opt) : new SimpleOAuthClient(opt);
return;
}

throw new InvalidOperationException();
}

public string GetAuthorizeUrl(string[] scopes, string redirectUri, string state,Dictionary<string,string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return _client.GetAuthorizeUrl(scopes, redirectUri, state, extData);
}

public async Task<AuthorizeResult?> AuthorizeWithCodeAsync(string code, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.AuthorizeWithCodeAsync(code, token, extData);
}

public async Task<DeviceCodeData?> GetCodePairAsync(string[] scopes, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.GetCodePairAsync(scopes, token, extData);
}

public async Task<AuthorizeResult?> AuthorizeWithDeviceAsync(DeviceCodeData data, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.AuthorizeWithDeviceAsync(data, token, extData);
}

public async Task<AuthorizeResult?> AuthorizeWithSilentAsync(AuthorizeResult data, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.AuthorizeWithSilentAsync(data, token, extData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace PCL.Core.Minecraft.IdentityModel.Extensions.OpenId;



public record OpenIdMetadata
{
[JsonPropertyName("issuer")]
public required string Issuer { get; init; }

[JsonPropertyName("authorization_endpoint")]
public string? AuthorizationEndpoint { get; init; }

[JsonPropertyName("device_authorization_endpoint")]
public string? DeviceAuthorizationEndpoint { get; init; }

[JsonPropertyName("token_endpoint")]
public required string TokenEndpoint { get; init; }

[JsonPropertyName("userinfo_endpoint")]
public required string UserInfoEndpoint { get; init; }

[JsonPropertyName("registration_endpoint")]
public string? RegistrationEndpoint { get; init; }

[JsonPropertyName("jwks_uri")]
public required string JwksUri { get; init; }

[JsonPropertyName("scopes_supported")]
public required IReadOnlyList<string> ScopesSupported { get; init; }

[JsonPropertyName("subject_types_supported")]
public required IReadOnlyList<string> SubjectTypesSupported { get; init; }

[JsonPropertyName("id_token_signing_alg_values_supported")]
public required IReadOnlyList<string> IdTokenSigningAlgValuesSupported { get; init; }


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using PCL.Core.Minecraft.IdentityModel.Extensions.JsonWebToken;
using PCL.Core.Minecraft.IdentityModel.OAuth;

namespace PCL.Core.Minecraft.IdentityModel.Extensions.OpenId;

public record OpenIdOptions(Func<HttpClient> GetHttpClient, string ConfigurationAddress)
{
public string OpenIdDiscoveryAddress => ConfigurationAddress;
public required string ClientId
{
get;
set;
}

public bool OnlyDeviceAuthorize { get; set; }

public string? RedirectUri;

public Dictionary<string, string>? Headers { get; set; }

public bool EnablePkceSupport { get; set; } = true;
public Func<HttpClient> GetClient => GetHttpClient;
public OpenIdMetadata? Meta { get; set; }



public virtual async Task InitiateAsync(CancellationToken token)
{
using var request = new HttpRequestMessage(HttpMethod.Get, OpenIdDiscoveryAddress);
if (Headers is not null)
foreach (var kvp in Headers)
{
_ = request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
}

var requestTask = GetClient.Invoke().SendAsync(request, token);
using var response = await requestTask;
var task = response.Content.ReadAsStringAsync(token);
Meta = JsonSerializer.Deserialize<OpenIdMetadata>(await task);
}

public async Task<JsonWebKey> GetSignatureKeyAsync(string kid,CancellationToken token)
{
if (Meta?.JwksUri is null) throw new InvalidOperationException();
using var request = new HttpRequestMessage(HttpMethod.Get, Meta.JwksUri);
if (Headers is not null)
foreach (var kvp in Headers)
{
_ = request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
}
using var response = await GetClient.Invoke().SendAsync(request, token);
var result = JsonSerializer.Deserialize<JsonWebKeys>(await response.Content.ReadAsStringAsync(token));
return result?.Keys.Single(k => k.Kid == kid)
?? throw new FormatException();
}

public virtual async Task<OAuthClientOptions> BuildOAuthOptionsAsync(CancellationToken token)
{
await InitiateAsync(token);
if(!OnlyDeviceAuthorize) ArgumentException.ThrowIfNullOrEmpty(RedirectUri);
return new OAuthClientOptions
{
GetClient = GetClient,
ClientId = ClientId,
RedirectUri = OnlyDeviceAuthorize ? string.Empty:RedirectUri!,
Meta = new EndpointMeta
{
AuthorizeEndpoint = Meta?.AuthorizationEndpoint??string.Empty,
DeviceEndpoint = Meta?.DeviceAuthorizationEndpoint??string.Empty,
TokenEndpoint = Meta!.TokenEndpoint,
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace PCL.Core.Minecraft.IdentityModel.Extensions.Pkce;

public enum PkceChallengeOptions
{
Sha256,
PlainText
}
58 changes: 58 additions & 0 deletions PCL.Core/Minecraft/IdentityModel/Extensions/Pkce/PkceClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using PCL.Core.Utils.Exts;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using PCL.Core.Minecraft.IdentityModel.OAuth;
using PCL.Core.Utils.Hash;

namespace PCL.Core.Minecraft.IdentityModel.Extensions.Pkce;

/// <summary>
/// 带 PKCE 支持的客户端 <br/>
/// 此客户端并非线程安全,请勿在多个线程间共享示例
/// </summary>
/// <param name="options"></param>
public class PkceClient(OAuthClientOptions options):IOAuthClient
{
private byte[] _ChallengeCode { get; set; } = new byte[32];
private bool _isCallGetAuthorizeUrl;
public PkceChallengeOptions ChallengeMethod { get; private set; } = PkceChallengeOptions.Sha256;
private readonly SimpleOAuthClient _client = new(options);
public string GetAuthorizeUrl(string[] scopes, string redirectUri, string state, Dictionary<string, string>? extData)
{
RandomNumberGenerator.Fill(_ChallengeCode);
var hash = SHA256Provider.Instance.ComputeHash(_ChallengeCode);
extData ??= [];
extData["code_challenge"] = hash;
extData["code_challenge_method"] = "S256";
_isCallGetAuthorizeUrl = true;
return _client.GetAuthorizeUrl(scopes, redirectUri, state, extData);
}

public async Task<AuthorizeResult?> AuthorizeWithCodeAsync(string code, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (!_isCallGetAuthorizeUrl) throw new InvalidOperationException("Challenge code is invalid");
var pkce = _ChallengeCode.FromBytesToB64UrlSafe();
extData ??= [];
extData["code_verifier"] = pkce;
_isCallGetAuthorizeUrl = false;
return await _client.AuthorizeWithCodeAsync(code, token, extData);
}

public async Task<DeviceCodeData?> GetCodePairAsync(string[] scopes, CancellationToken token, Dictionary<string, string>? extData = null)
{
return await _client.GetCodePairAsync(scopes, token, extData);
}

public async Task<AuthorizeResult?> AuthorizeWithDeviceAsync(DeviceCodeData data, CancellationToken token, Dictionary<string, string>? extData = null)
{
return await _client.AuthorizeWithDeviceAsync(data, token, extData);
}

public async Task<AuthorizeResult?> AuthorizeWithSilentAsync(AuthorizeResult data, CancellationToken token, Dictionary<string, string>? extData = null)
{
return await _client.AuthorizeWithSilentAsync(data, token, extData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using PCL.Core.Minecraft.IdentityModel.Extensions.OpenId;
using PCL.Core.Minecraft.IdentityModel.OAuth;

namespace PCL.Core.Minecraft.IdentityModel.Extensions.YggdrasilConnect;

// Steven Qiu 说这东西完全就是 OpenId + 魔改了一部分,所以可以直接复用 OpenId 的逻辑

/// <summary>
///
/// </summary>
public class YggdrasilClient:IOAuthClient
{

private OpenIdClient? _client;

private YggdrasilOptions _options;

public YggdrasilClient(YggdrasilOptions options)
{
_options = options;
}
/// <summary>
///
/// </summary>
/// <exception cref="ArgumentException">当无法获取 ClientId 时抛出,调用方应该设置 ClientId 并重新实例化 OpenId Client</exception>
/// <param name="token"></param>
public async Task InitialAsync(CancellationToken token)
{
_client = new OpenIdClient(_options);
await _client.InitialAsync(token);
}
/// <summary>
/// 获取授权端点地址
/// </summary>
/// <param name="scopes"></param>
/// <param name="redirectUri"></param>
/// <param name="state"></param>
/// <param name="extData"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">未调用 <see cref="InitialAsync"/></exception>
public string GetAuthorizeUrl(string[] scopes, string redirectUri, string state, Dictionary<string, string>? extData)
{
if (_client is null) throw new InvalidOperationException();
return _client.GetAuthorizeUrl(scopes, redirectUri, state, extData);
}

public async Task<AuthorizeResult?> AuthorizeWithCodeAsync(string code, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.AuthorizeWithCodeAsync(code, token, extData);

}

public async Task<DeviceCodeData?> GetCodePairAsync(string[] scopes, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.GetCodePairAsync(scopes, token, extData);

}

public async Task<AuthorizeResult?> AuthorizeWithDeviceAsync(DeviceCodeData data, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.AuthorizeWithDeviceAsync(data, token, extData);

}

public async Task<AuthorizeResult?> AuthorizeWithSilentAsync(AuthorizeResult data, CancellationToken token, Dictionary<string, string>? extData = null)
{
if (_client is null) throw new InvalidOperationException();
return await _client.AuthorizeWithSilentAsync(data, token, extData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
using PCL.Core.Minecraft.IdentityModel.Extensions.OpenId;

namespace PCL.Core.Minecraft.IdentityModel.Extensions.YggdrasilConnect;

public record YggdrasilConnectMetaData: OpenIdMetadata
{
[JsonPropertyName("shared_client_id")]
public string? SharedClientId { get; init; }
}
Loading