diff --git a/Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/JwtDecoder.cs b/Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/JwtDecoder.cs index 6bbe26b..eebc0c5 100644 --- a/Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/JwtDecoder.cs +++ b/Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/JwtDecoder.cs @@ -4,6 +4,7 @@ namespace OpenDive.Utils.Jwt using System.Text; using UnityEngine; using Newtonsoft.Json; + using System.Collections.Generic; /// /// A class to decode JWT tokens. @@ -56,6 +57,58 @@ public static JWT DecodeJWT(string token) } } + /// + /// Utility class to decode non-required claims. + /// + /// The JWT token string + /// Parsed claims + private static void ParseCustomClaims(string json, Dictionary claims) + { + // Simple JSON parsing for claims + // Remove the first and last curly braces and split by commas + json = json.Trim('{', '}'); + string[] pairs = json.Split(','); + + foreach (string pair in pairs) + { + try + { + string[] keyValue = pair.Split(':'); + if (keyValue.Length == 2) + { + string key = keyValue[0].Trim('"', ' '); + string value = keyValue[1].Trim('"', ' '); + + // Skip standard claims + if (!IsStandardClaim(key)) + { + claims[key] = value; + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to parse claim pair: {pair}. Error: {ex.Message}"); + } + } + } + + /// + /// Utility class to check against standard claims. + /// + /// + /// + private static bool IsStandardClaim(string claimName) + { + return claimName == "iss" || + claimName == "sub" || + claimName == "aud" || + claimName == "exp" || + claimName == "nbf" || + claimName == "iat" || + claimName == "jti"; + } + /// /// Decodes a Base64 URL-encoded string. /// @@ -80,8 +133,46 @@ private static string Base64UrlDecode(string base64Url) /// public class JWT { + /// + /// The JOSE (JSON Object Signing and Encryption) Header is comprised + /// of a set of Header Parameters that typically consist of a name/value pair: + /// the hashing algorithm being used (e.g., HMAC SHA256 or RSA) and the type of the JWT. + /// public JWTHeader Header { get; set; } + /// + /// JWS payload (set of claims): contains verifiable security statements. + /// These are statements about an entity (typically, the user) and additional data. + /// There are three types of claims: registered, public, and private claims. + /// + /// Registered claims: These are a set of predefined claims which are + /// not mandatory but recommended, to provide a set of useful, interoperable claims. + /// Some of them are: iss (issuer), exp (expiration time), sub (subject), aud (audience), and others. + /// + /// Public claims: These can be defined at will by those using JWTs. + /// But to avoid collisions they should be defined in the IANA JSON Web Token Registry + /// or be defined as a URI that contains a collision resistant namespace. + /// + /// Private claims: These are the custom claims created to share + /// information between parties that agree on using them and are + /// neither registered or public claims. + /// public JWTPayload Payload { get; set; } + /// + /// The signature is used to verify that the sender of the JWT is who + /// it says it is and to ensure that the message wasn't changed along the way. + /// To create the signature, the Base64-encoded header and payload are taken, + /// along with a secret, and signed with the algorithm specified in the header. + /// + /// When you use a JWT, you must check its signature before storing and using it. + /// + /// For example, if you are creating a signature for a token using the + /// HMAC SHA256 algorithm, you would do the following: + /// HMACSHA256( + /// base64UrlEncode(header) + "." + + /// base64UrlEncode(payload), + /// secret) + /// + /// public string Signature { get; set; } } @@ -90,8 +181,9 @@ public class JWT /// public class JWTHeader { - public string alg { get; set; } // Algorithm - public string typ { get; set; } // Token type + public string alg { get; set; } // * Algorithm. Required for ZK Login. + public string typ { get; set; } // * Token type. Required for ZK Login. + public string kid { get; set; } // * The kid value indicates what key was used to sign the JWT. Required for ZK Login. } /// @@ -99,16 +191,22 @@ public class JWTHeader /// public class JWTPayload { - public string Iss { get; set; } // Issuer - public string Sub { get; set; } // Subject - public string Aud { get; set; } // Audience + // <> Registered claims + public string Iss { get; set; } // * Issuer of the JWT. Required for ZK Login. + public string Sub { get; set; } // * Subject of the JWT (the user). Required for ZK Login. + public string Aud { get; set; } // Recipient for which the JWT is intended + public long? Exp { get; set; } // Time after which the JWT expires + public long? Nbf { get; set; } // Time before which the JWT must not be accepted for processing (Unix timestamp) + public long? Iat { get; set; } // Issued at .. Time at which the JWT was issued; can be used to determine age of the JWT (Unix timestamp) + public string Jti { get; set; } // JWT ID (unique identifier for the token). Can be used to prevent the JWT from being replayed (allows a token to be used only once) + + // <> Public claims public long? Auth_time { get; set; } // Authentication time (Unix timestamp) - public long? Iat { get; set; } // Issued at (Unix timestamp) - public long? Exp { get; set; } // Expiration time (Unix timestamp) - public string Email { get; set; } // Email address - public string Name { get; set; } // Name - public string Jti { get; set; } // JWT ID (unique identifier for the token) + public string Nonce { get; set; } // * Required for ZK Login. Used instead of `iat` and `exp`. + // <> Custom claims + public string Email { get; set; } // Email address + public string Name { get; set; } // Name // Add additional properties as needed based on your JWT structure. } diff --git a/Assets/Sui-Unity-SDK/Tests/JwtDecoderTest.cs b/Assets/Sui-Unity-SDK/Tests/JwtDecoderTest.cs new file mode 100644 index 0000000..42a17fb --- /dev/null +++ b/Assets/Sui-Unity-SDK/Tests/JwtDecoderTest.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using OpenDive.Utils.Jwt; + +namespace Sui.Tests +{ + [TestFixture] + public class JwDecoderTest + { + string jwt_sample_1 + = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODUxNDA5ODQsImlhdCI6MTQ4NTEzNzM4NCwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiIyOWFjMGMxOC0wYjRhLTQyY2YtODJmYy0wM2Q1NzAzMThhMWQiLCJhcHBsaWNhdGlvbklkIjoiNzkxMDM3MzQtOTdhYi00ZDFhLWFmMzctZTAwNmQwNWQyOTUyIiwicm9sZXMiOltdfQ.Mp0Pcwsz5VECK11Kf2ZZNF_SMKu5CgBeLN9ZOP04kZo\n"; + [Test] + public void DecodeJwtTest() + { + JWT jwtDecoded = JWTDecoder.DecodeJWT(jwt_sample_1); + + JWTHeader header = jwtDecoded.Header; + string algo_expected = "HS256"; + string typ_expected = "JWT"; + Assert.AreEqual(algo_expected, header.alg); + Assert.AreEqual(typ_expected, header.typ); + + JWTPayload payload = jwtDecoded.Payload; + long exp_expected = 1485140984; // (long)payload.Exp; + long iat_expected = 1485137384; + string iss_expected = "acme.com"; + string sub_expected = "29ac0c18-0b4a-42cf-82fc-03d570318a1d"; + string application_id = "79103734-97ab-4d1a-af37-e006d05d2952"; + string[] roles_expected = { }; + Assert.AreEqual(exp_expected, payload.Exp); + Assert.AreEqual(iat_expected, payload.Iat); + Assert.AreEqual(iss_expected, payload.Iss); + Assert.AreEqual(sub_expected, payload.Sub); + //Assert.AreEqual(application_id, payload...) + } + } + +} \ No newline at end of file diff --git a/Assets/Sui-Unity-SDK/Tests/JwtDecoderTest.cs.meta b/Assets/Sui-Unity-SDK/Tests/JwtDecoderTest.cs.meta new file mode 100644 index 0000000..a7ea96b --- /dev/null +++ b/Assets/Sui-Unity-SDK/Tests/JwtDecoderTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 75c6c2d8312c94a0cbd1ca402444deb0 \ No newline at end of file diff --git a/Assets/Sui-Unity-SDK/Tests/JwtUtilsTests.cs b/Assets/Sui-Unity-SDK/Tests/JwtUtilsTests.cs index 1a5ba85..5f02d3e 100644 --- a/Assets/Sui-Unity-SDK/Tests/JwtUtilsTests.cs +++ b/Assets/Sui-Unity-SDK/Tests/JwtUtilsTests.cs @@ -2,110 +2,113 @@ using NUnit.Framework; using Sui.ZKLogin; -[TestFixture] -public class JwtUtilsTests +namespace Sui.Tests { - [Test] - public void ExtractClaimValue_ValidClaim_ReturnsCorrectValue() + [TestFixture] + public class JwtUtilsTests { - // Arrange - var claim = new Claim + [Test] + public void ExtractClaimValue_ValidClaim_ReturnsCorrectValue() { - value = "eyJuYW1lIjoiSm9obiJ9", // Base64URL encoded '{"name":"John"}' - indexMod4 = 0 - }; - - // Act - string result = JwtUtils.ExtractClaimValue(claim, "name"); - - // Assert - Assert.AreEqual("John", result); - } - - [Test] - public void ExtractClaimValue_InvalidIndex_ThrowsException() - { - var claim = new Claim + // Arrange + var claim = new Claim + { + value = "eyJuYW1lIjoiSm9obiJ9", // Base64URL encoded '{"name":"John"}' + indexMod4 = 0 + }; + + // Act + string result = JwtUtils.ExtractClaimValue(claim, "name"); + + // Assert + Assert.AreEqual("John", result); + } + + [Test] + public void ExtractClaimValue_InvalidIndex_ThrowsException() { - value = "eyJuYW1lIjoiSm9obiJ9", - indexMod4 = 3 // Invalid index - }; - - Assert.Throws(() => - JwtUtils.ExtractClaimValue(claim, "name")); - } - - [Test] - public void ExtractClaimValue_WrongClaimName_ThrowsException() - { - var claim = new Claim + var claim = new Claim + { + value = "eyJuYW1lIjoiSm9obiJ9", + indexMod4 = 3 // Invalid index + }; + + Assert.Throws(() => + JwtUtils.ExtractClaimValue(claim, "name")); + } + + [Test] + public void ExtractClaimValue_WrongClaimName_ThrowsException() { - value = "eyJuYW1lIjoiSm9obiJ9", - indexMod4 = 0 - }; - - Assert.Throws(() => - JwtUtils.ExtractClaimValue(claim, "wrongName")); - } - - [Test] - public void ExtractClaimValue_ComplexObject_DeserializesCorrectly() - { - var claim = new Claim + var claim = new Claim + { + value = "eyJuYW1lIjoiSm9obiJ9", + indexMod4 = 0 + }; + + Assert.Throws(() => + JwtUtils.ExtractClaimValue(claim, "wrongName")); + } + + [Test] + public void ExtractClaimValue_ComplexObject_DeserializesCorrectly() { - value = "eyJ1c2VyIjp7Im5hbWUiOiJKb2huIiwiYWdlIjozMH19", - indexMod4 = 0 - }; + var claim = new Claim + { + value = "eyJ1c2VyIjp7Im5hbWUiOiJKb2huIiwiYWdlIjozMH19", + indexMod4 = 0 + }; - var result = JwtUtils.ExtractClaimValue(claim, "user"); + var result = JwtUtils.ExtractClaimValue(claim, "user"); - Assert.AreEqual("John", result.name); - Assert.AreEqual(30, result.age); - } + Assert.AreEqual("John", result.name); + Assert.AreEqual(30, result.age); + } - [Test] - public void ExtractClaimValue_ShortInput_ThrowsException() - { - var claim = new Claim + [Test] + public void ExtractClaimValue_ShortInput_ThrowsException() { - value = "a", // Too short - indexMod4 = 0 - }; - - Assert.Throws(() => - JwtUtils.ExtractClaimValue(claim, "test")); - } - - [Test] - public void ExtractClaimValue_InvalidBase64Url_ThrowsException() - { - var claim = new Claim + var claim = new Claim + { + value = "a", // Too short + indexMod4 = 0 + }; + + Assert.Throws(() => + JwtUtils.ExtractClaimValue(claim, "test")); + } + + [Test] + public void ExtractClaimValue_InvalidBase64Url_ThrowsException() { - value = "!@#$%^", // Invalid characters - indexMod4 = 0 - }; - - Assert.Throws(() => - JwtUtils.ExtractClaimValue(claim, "test")); + var claim = new Claim + { + value = "!@#$%^", // Invalid characters + indexMod4 = 0 + }; + + Assert.Throws(() => + JwtUtils.ExtractClaimValue(claim, "test")); + } + + [Test] + public void ExtractClaimValue_MultipleKeysInJson_ThrowsException() + { + var claim = new Claim + { + value = "eyJrZXkxIjoidmFsdWUxIiwia2V5MiI6InZhbHVlMiJ9", + indexMod4 = 0 + }; + + Assert.Throws(() => + JwtUtils.ExtractClaimValue(claim, "key1")); + } } - [Test] - public void ExtractClaimValue_MultipleKeysInJson_ThrowsException() + [Serializable] + public class UserData { - var claim = new Claim - { - value = "eyJrZXkxIjoidmFsdWUxIiwia2V5MiI6InZhbHVlMiJ9", - indexMod4 = 0 - }; - - Assert.Throws(() => - JwtUtils.ExtractClaimValue(claim, "key1")); + public string name; + public int age; } -} - -[Serializable] -public class UserData -{ - public string name; - public int age; } \ No newline at end of file