diff --git a/README.md b/README.md index 9d0b76c..3d21c64 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Godot Engine GDScript JWT JSON Web Token library for Godot Engine written in GDScript -## Create JWT +## Create HS256 JWT ```gdscript var secret: String = JWTAlgorithmBuilder.random_secret(5) var jwt_algorithm: JWTAlgorithm = JWTAlgorithmBuilder.HS256(secret) @@ -12,34 +12,66 @@ var jwt_builder: JWTBuilder = JWT.create() \ var jwt: String = jwt_builder.sign(jwt_algorithm) ``` -## Decode JWT +## Verify HS256 JWT ```gdscript var jwt: String = "" -var jwt_decoder: JWTDecoder = JWT.decode(jwt) -# Get the JWT as an Array -print("%s.%s.%s" % jwt_decoder.parts) -# Decode a specific part -print(JWTUtils.base64URL_decode(jwt_decoder.get_payload())) +var secret: String = "" +var jwt_algorithm: JWTAlgorithm = JWTAlgorithmBuilder.HS256(secret) +var jwt_verifier: JWTVerifier = JWT.require(jwt_algorithm) \ + .with_claim("my-claim","my-value") \ + .build() # Reusable Verifier +if jwt_verifier.verify(jwt) == JWTVerifier.JWTExceptions.OK : + print("Verified!") +else: + print(jwt_verifier.exception) ``` -## Verify JWT +## Create RS256 JWT ```gdscript +var private_key : CryptoKey = crypto.generate_rsa(4096) +var public_key : CryptoKey = CryptoKey.new() +public_key.load_from_string(private_key.save_to_string(true)) + +var jwt_algorithm: JWTAlgorithm = JWTAlgorithmBuilder.RS256(public_key, private_key) +var jwt_builder: JWTBuilder = JWT.create() \ + .with_expires_at(OS.get_unix_time()) \ + .with_issuer("Godot") \ + .with_claim("id","someid") +var jwt: String = jwt_builder.sign(jwt_algorithm) +``` + +## Verify RS256 JWT +```gdscript +var private_key: CryptoKey = CryptoKey.new() +var public_key: CryptoKey = CryptoKey.new() +private_key.load_from_string("", false) +public_key.load_from_string("", true) + var jwt: String = "" -var secret: String = "" -var jwt_algorithm: JWTAlgorithm = JWTAlgorithmBuilder.HS256(secret) +var jwt_algorithm: JWTAlgorithm = JWTAlgorithmBuilder.RS256(public_key) var jwt_verifier: JWTVerifier = JWT.require(jwt_algorithm) \ -.with_claim("my-claim","my-value") \ -.build() # Reusable Verifier -if jwt_verifier.verify(jwt) == JWTVerifier.Exceptions.OK : + .with_claim("id","someid") \ + .build() # Reusable Verifier +if jwt_verifier.verify(jwt) == JWTVerifier.JWTExceptions.OK : print("Verified!") else: print(jwt_verifier.exception) ``` +## Decode JWT +```gdscript +var jwt: String = "" +var jwt_decoder: JWTDecoder = JWT.decode(jwt) +# Get the JWT as an Array +print("%s.%s.%s" % jwt_decoder.parts) +# Decode a specific part +print(JWTUtils.base64URL_decode(jwt_decoder.get_payload())) +``` + ### JWT Utils ```gdscript -JWTUtils.base64URL_encode(bytes: PoolByteArray) -> String -JWTUtils.base64URL_decode(string: String) -> String +JWTUtils.base64URL_encode(bytes: PackedByteArray) -> String +JWTUtils.base64URL_decode(string: String) -> PackedByteArray ``` #### Supported Algorithms diff --git a/addons/jwt/plugin.cfg b/addons/jwt/plugin.cfg index 59e59d9..428c592 100644 --- a/addons/jwt/plugin.cfg +++ b/addons/jwt/plugin.cfg @@ -3,5 +3,5 @@ name="JWT" description="" author="Nicolò (fenix-hub) Santilio" -version="1.5" +version="1.0" script="plugin.gd" diff --git a/addons/jwt/plugin.gd b/addons/jwt/plugin.gd index 43c1418..531675f 100644 --- a/addons/jwt/plugin.gd +++ b/addons/jwt/plugin.gd @@ -1,4 +1,4 @@ -tool +@tool extends EditorPlugin diff --git a/addons/jwt/src/JWT.gd b/addons/jwt/src/JWT.gd index 1c7a247..e17486c 100644 --- a/addons/jwt/src/JWT.gd +++ b/addons/jwt/src/JWT.gd @@ -1,4 +1,4 @@ -extends Reference +extends RefCounted class_name JWT static func create(algorithm: JWTAlgorithm = null, header_claims: Dictionary = {}, payload_claims: Dictionary = {}) -> JWTBuilder: diff --git a/addons/jwt/src/JWTAlgorithm.gd b/addons/jwt/src/JWTAlgorithm.gd index 418af45..5f119b6 100644 --- a/addons/jwt/src/JWTAlgorithm.gd +++ b/addons/jwt/src/JWTAlgorithm.gd @@ -1,4 +1,4 @@ -extends Reference +extends RefCounted class_name JWTAlgorithm enum Type { @@ -7,7 +7,7 @@ enum Type { RSA256 } -var _hash: int = -1 +var _alg: int = -1 var _secret: String = "" var crypto: Crypto = Crypto.new() @@ -15,30 +15,47 @@ var _public_crypto: CryptoKey = CryptoKey.new() var _private_crypto: CryptoKey = CryptoKey.new() func get_name() -> String: - match _hash: + match _alg: + # Note: HS1 is not secure and should be removed. Type.HMAC1: return "HSA1" - Type.HMAC256: return "HSA256" - Type.RSA256: return "RSA256" + Type.HMAC256: return "HS256" + Type.RSA256: return "RS256" _: return "" -func sign(text: String) -> PoolByteArray: - var signature_bytes: PoolByteArray = [] - match self._hash: + +func _digest(ctx_type: HashingContext.HashType, data: PackedByteArray) -> PackedByteArray: + var ctx = HashingContext.new() + # Start a SHA-256 context. + ctx.start(ctx_type) + # Check that file exists. + ctx.update(data) + # Get the computed hash. + return ctx.finish() + + +func sign(text: String) -> PackedByteArray: + var signature_bytes: PackedByteArray = [] + match self._alg: Type.HMAC1: - signature_bytes = self.crypto.hmac_digest(HashingContext.HASH_SHA1, self._secret.to_utf8(), text.to_utf8()) + signature_bytes = self.crypto.hmac_digest(HashingContext.HASH_SHA1, self._secret.to_utf8_buffer(), text.to_utf8_buffer()) Type.HMAC256: - signature_bytes = self.crypto.hmac_digest(HashingContext.HASH_SHA256, self._secret.to_utf8(), text.to_utf8()) + signature_bytes = self.crypto.hmac_digest(HashingContext.HASH_SHA256, self._secret.to_utf8_buffer(), text.to_utf8_buffer()) Type.RSA256: - signature_bytes = self.crypto.encrypt(self._private_crypto, text.to_utf8()) + signature_bytes = self.crypto.sign(HashingContext.HASH_SHA256, text.sha256_buffer(), self._private_crypto) return signature_bytes + +# TODO: Debug this. func verify(jwt: JWTDecoder) -> bool: - var signature_bytes: PoolByteArray = [] - match self._hash: + var signature_bytes: PackedByteArray = [] + match self._alg: Type.HMAC1: - signature_bytes = crypto.hmac_digest(HashingContext.HASH_SHA1, self._secret.to_utf8(), (jwt.parts[0]+"."+jwt.parts[1]).to_utf8()) + signature_bytes = self.crypto.hmac_digest(HashingContext.HASH_SHA1, self._secret.to_utf8_buffer(), (jwt.parts[0]+"."+jwt.parts[1]).to_utf8_buffer()) Type.HMAC256: - signature_bytes = crypto.hmac_digest(HashingContext.HASH_SHA256, self._secret.to_utf8(), (jwt.parts[0]+"."+jwt.parts[1]).to_utf8()) + signature_bytes = self.crypto.hmac_digest(HashingContext.HASH_SHA256, self._secret.to_utf8_buffer(), (jwt.parts[0]+"."+jwt.parts[1]).to_utf8_buffer()) Type.RSA256: - signature_bytes = self.crypto.decrypt(self._public_crypto, (jwt.parts[0]+"."+jwt.parts[1]).to_utf8()) + # type, hash, sig, key + print() + return self.crypto.verify(HashingContext.HASH_SHA256, (jwt.parts[0]+"."+jwt.parts[1]).sha256_buffer(), JWTUtils.base64URL_decode(jwt.parts[2]), self._public_crypto) + #signature_bytes = self.crypto.verify(self._public_crypto, .to_utf8_buffer()) return jwt.parts[2] == JWTUtils.base64URL_encode(signature_bytes) diff --git a/addons/jwt/src/JWTAlgorithmBuilder.gd b/addons/jwt/src/JWTAlgorithmBuilder.gd index 20310ac..6e5d78d 100644 --- a/addons/jwt/src/JWTAlgorithmBuilder.gd +++ b/addons/jwt/src/JWTAlgorithmBuilder.gd @@ -1,29 +1,48 @@ -extends Reference +extends RefCounted class_name JWTAlgorithmBuilder + static func random_secret(length: int = 10) -> String: - return Crypto.new().generate_random_bytes(10).get_string_from_utf8() + return Crypto.new().generate_random_bytes(length).get_string_from_utf8() + static func HSA1(secret: String) -> JWTAlgorithm: var algorithm: JWTAlgorithm = JWTAlgorithm.new() algorithm._secret = secret - algorithm._hash = JWTAlgorithm.Type.HMAC1 + algorithm._alg = JWTAlgorithm.Type.HMAC1 return algorithm + +static func HS1(secret: String) -> JWTAlgorithm: + return HSA1(secret) + + static func HSA256(secret: String) -> JWTAlgorithm: var algorithm: JWTAlgorithm = JWTAlgorithm.new() algorithm._secret = secret - algorithm._hash = JWTAlgorithm.Type.HMAC256 + algorithm._alg = JWTAlgorithm.Type.HMAC256 return algorithm -static func RSA256(public_key: CryptoKey, private_key: CryptoKey) -> JWTAlgorithm: + +static func HS256(secret: String) -> JWTAlgorithm: + return HSA256(secret) + + +static func RSA256(public_key: CryptoKey, private_key: CryptoKey = CryptoKey.new()) -> JWTAlgorithm: var algorithm: JWTAlgorithm = JWTAlgorithm.new() algorithm._public_crypto = public_key algorithm._private_crypto = private_key + algorithm._alg = JWTAlgorithm.Type.RSA256 return algorithm -static func sign(text: String, algorithm: JWTAlgorithm) -> PoolByteArray: + +static func RS256(public_key: CryptoKey, private_key: CryptoKey) -> JWTAlgorithm: + return RSA256(public_key, private_key) + + +static func sign(text: String, algorithm: JWTAlgorithm) -> PackedByteArray: return algorithm.sign(text) + static func verify(jwt: JWTDecoder, algorithm: JWTAlgorithm) -> bool: return algorithm.verify(jwt) diff --git a/addons/jwt/src/JWTBaseBuilder.gd b/addons/jwt/src/JWTBaseBuilder.gd index 3172078..09592cf 100644 --- a/addons/jwt/src/JWTBaseBuilder.gd +++ b/addons/jwt/src/JWTBaseBuilder.gd @@ -1,4 +1,4 @@ -extends Reference +extends RefCounted class_name JWTBaseBuilder func with_header(header_claims: Dictionary) -> JWTBaseBuilder: @@ -25,7 +25,7 @@ func with_subject(subject: String) -> JWTBaseBuilder: add_claim(JWTClaims.Public.SUBJECT, subject) return self -func with_audience(audience: PoolStringArray) -> JWTBaseBuilder: +func with_audience(audience: PackedStringArray) -> JWTBaseBuilder: add_claim(JWTClaims.Public.AUDIENCE, audience) return self diff --git a/addons/jwt/src/JWTBuilder.gd b/addons/jwt/src/JWTBuilder.gd index 3f941f9..b08eeec 100644 --- a/addons/jwt/src/JWTBuilder.gd +++ b/addons/jwt/src/JWTBuilder.gd @@ -8,10 +8,12 @@ var header_claims: Dictionary = { alg = "", typ = "JWT" } var payload_claims: Dictionary var secret: String -func _init(algorithm: JWTAlgorithm = null, header_claims: Dictionary = {}, payload_claims: Dictionary = {}): - if algorithm != null: self.algorithm = algorithm - if not header_claims.empty(): self.header_claims = header_claims - if not payload_claims.empty(): self.payload_claims = payload_claims +func _init(algorithm_param: JWTAlgorithm = null, header_claims_param: Dictionary = {}, payload_claims_param: Dictionary = {}): + if not header_claims_param.is_empty(): self.header_claims = header_claims_param + if not payload_claims_param.is_empty(): self.payload_claims = payload_claims_param + if algorithm_param != null: + self.algorithm = algorithm_param + self.header_claims.alg = self.algorithm.get_name() func add_claim(name: String, value) -> void: match typeof(value): @@ -34,8 +36,10 @@ func sign(algorithm: JWTAlgorithm = null) -> String: if algorithm != null: self.algorithm = algorithm assert(algorithm != null, "Can't sign a JWT without an Algorithm") with_algorithm(algorithm.get_name()) - var header: String = JWTUtils.base64URL_encode(JSON.print(self.header_claims).to_utf8()) - var payload: String = JWTUtils.base64URL_encode(JSON.print(self.payload_claims).to_utf8()) - var signature_bytes: PoolByteArray = algorithm.sign(header+"."+payload) + var header_serializer : JSON = JSON.new() + var header: String = JWTUtils.base64URL_encode(header_serializer.stringify(self.header_claims).to_utf8_buffer()) + var payload_serializer : JSON = JSON.new() + var payload: String = JWTUtils.base64URL_encode(payload_serializer.stringify(self.payload_claims).to_utf8_buffer()) + var signature_bytes: PackedByteArray = algorithm.sign(header+"."+payload) var signature: String = JWTUtils.base64URL_encode(signature_bytes) return "%s.%s.%s" % [header, payload, signature] diff --git a/addons/jwt/src/JWTClaims.gd b/addons/jwt/src/JWTClaims.gd index fdba7f1..1433ff9 100644 --- a/addons/jwt/src/JWTClaims.gd +++ b/addons/jwt/src/JWTClaims.gd @@ -1,4 +1,4 @@ -extends Reference +extends RefCounted class_name JWTClaims diff --git a/addons/jwt/src/JWTDecoder.gd b/addons/jwt/src/JWTDecoder.gd index 5b19292..ae96071 100644 --- a/addons/jwt/src/JWTDecoder.gd +++ b/addons/jwt/src/JWTDecoder.gd @@ -1,4 +1,4 @@ -extends Reference +extends RefCounted class_name JWTDecoder var parts: Array = [] @@ -7,16 +7,16 @@ var payload_claims: Dictionary = {} func _init(jwt: String): self.parts = jwt.split(".") - var header: String = JWTUtils.base64URL_decode(self.parts[0]) - var payload: String = JWTUtils.base64URL_decode(self.parts[1]) - self.header_claims = _parse_json(header) - self.payload_claims = _parse_json(payload) + var header: PackedByteArray = JWTUtils.base64URL_decode(self.parts[0]) + var payload: PackedByteArray = JWTUtils.base64URL_decode(self.parts[1]) + self.header_claims = _parse_json(header.get_string_from_utf8()) + self.payload_claims = _parse_json(payload.get_string_from_utf8()) func _parse_json(field) -> Dictionary: - var parse_result: JSONParseResult = JSON.parse(field) - if parse_result.error != OK: + var parse_result = JSON.new() + if parse_result.parse(field) != OK: return {} - return parse_result.result + return parse_result.get_data() func get_algorithm() -> String: return self.header_claims.get(JWTClaims.Public.ALGORITHM, "null") @@ -42,7 +42,7 @@ func get_issuer() -> String: func get_subject() -> String: return self.payload_claims.get(JWTClaims.Public.SUBJECT, "null") -func get_audience() -> PoolStringArray: +func get_audience() -> PackedByteArray: return self.payload_claims.get(JWTClaims.Public.AUDIENCE, "null") func get_expires_at() -> int: diff --git a/addons/jwt/src/JWTUtils.gd b/addons/jwt/src/JWTUtils.gd index 297aef0..b617ea5 100644 --- a/addons/jwt/src/JWTUtils.gd +++ b/addons/jwt/src/JWTUtils.gd @@ -1,11 +1,11 @@ -extends Reference +extends RefCounted class_name JWTUtils -static func base64URL_encode(input: PoolByteArray) -> String: +static func base64URL_encode(input: PackedByteArray) -> String: return Marshalls.raw_to_base64(input).replacen("+","-").replacen("/","_").replacen("=","") -static func base64URL_decode(input: String) -> String: +static func base64URL_decode(input: String) -> PackedByteArray: match (input.length() % 4): 2: input += "==" 3: input += "=" - return Marshalls.base64_to_utf8(input.replacen("_","/").replacen("-","+")) + return Marshalls.base64_to_raw(input.replacen("_","/").replacen("-","+")) diff --git a/addons/jwt/src/JWTVerifier.gd b/addons/jwt/src/JWTVerifier.gd index 64a4c6b..a03d0f8 100644 --- a/addons/jwt/src/JWTVerifier.gd +++ b/addons/jwt/src/JWTVerifier.gd @@ -1,7 +1,7 @@ -extends Reference +extends RefCounted class_name JWTVerifier -enum Exceptions { +enum JWTExceptions { OK, INVALID_HEADER, INVALID_PAYLOAD, @@ -35,15 +35,15 @@ func verify_claim_values(jwt_decoder: JWTDecoder, expected_claims: Dictionary) - match claim: JWTClaims.Public.EXPRIES_AT: if not assert_valid_date_claim(jwt_decoder.get_expires_at(), expected_claims.get(claim), true): - self.exception = "The Token has expired on %s." % OS.get_datetime_from_unix_time(jwt_decoder.get_expires_at()) + self.exception = "The Token has expired on %s." % Time.get_datetime_string_from_unix_time(jwt_decoder.get_expires_at()) return false JWTClaims.Public.ISSUED_AT: if not assert_valid_date_claim(jwt_decoder.get_issued_at(), expected_claims.get(claim), false): - self.exception = "The Token can't be used before %s." % OS.get_datetime_from_unix_time(jwt_decoder.get_expires_at()) + self.exception = "The Token can't be used before %s." % Time.get_datetime_string_from_unix_time(jwt_decoder.get_expires_at()) return false JWTClaims.Public.NOT_BEFORE: if not assert_valid_date_claim(jwt_decoder.get_not_before(), expected_claims.get(claim), false): - self.exception = "The Token can't be used before %s." % OS.get_datetime_from_unix_time(jwt_decoder.get_expires_at()) + self.exception = "The Token can't be used before %s." % Time.get_datetime_string_from_unix_time(jwt_decoder.get_expires_at()) return false JWTClaims.Public.ISSUER: if not jwt_decoder.get_issuer() == expected_claims.get(claim): @@ -83,18 +83,18 @@ func assert_valid_date_claim(date: int, leeway: int, should_be_future: bool) -> func assert_valid_header(jwt_decoder: JWTDecoder) -> bool: self.exception = "The header is empty or invalid." - return not jwt_decoder.header_claims.empty() + return not jwt_decoder.header_claims.is_empty() func assert_valid_payload(jwt_decoder: JWTDecoder) -> bool: self.exception = "The payload is empty or invalid." - return not jwt_decoder.payload_claims.empty() + return not jwt_decoder.payload_claims.is_empty() -func verify(jwt: String) -> int: +func verify(jwt: String) -> JWTExceptions: self.jwt_decoder = JWTDecoder.new(jwt) - if not assert_valid_header(self.jwt_decoder): return Exceptions.INVALID_HEADER - if not assert_valid_payload(self.jwt_decoder): return Exceptions.INVALID_PAYLOAD - if not verify_algorithm(self.jwt_decoder, algorithm): return Exceptions.ALGORITHM_MISMATCHING - if not verify_signature(self.jwt_decoder): return Exceptions.INVALID_SIGNATURE - if not verify_claim_values(self.jwt_decoder, self.claims): return Exceptions.CLAIM_NOT_VALID + if not assert_valid_header(self.jwt_decoder): return JWTExceptions.INVALID_HEADER + if not assert_valid_payload(self.jwt_decoder): return JWTExceptions.INVALID_PAYLOAD + if not verify_algorithm(self.jwt_decoder, algorithm): return JWTExceptions.ALGORITHM_MISMATCHING + if not verify_signature(self.jwt_decoder): return JWTExceptions.INVALID_SIGNATURE + if not verify_claim_values(self.jwt_decoder, self.claims): return JWTExceptions.CLAIM_NOT_VALID self.exception = "" - return Exceptions.OK + return JWTExceptions.OK diff --git a/addons/jwt/src/JWTVerifierBuilder.gd b/addons/jwt/src/JWTVerifierBuilder.gd index 636fbfe..30a1620 100644 --- a/addons/jwt/src/JWTVerifierBuilder.gd +++ b/addons/jwt/src/JWTVerifierBuilder.gd @@ -4,7 +4,7 @@ class_name JWTVerifierBuilder var algorithm: JWTAlgorithm var claims: Dictionary = {} var leeway: int = 0 -var ignore_issued_at: bool = false +var _ignore_issued_at: bool = false func _init(algorithm: JWTAlgorithm): self.algorithm = algorithm @@ -25,11 +25,11 @@ func add_claim(name: String, value) -> void: return self.claims[name] = value -func with_any_of_issuers(issuers: PoolStringArray) -> JWTVerifierBuilder: +func with_any_of_issuers(issuers: PackedStringArray) -> JWTVerifierBuilder: add_claim(JWTClaims.Public.ISSUER, issuers) return self -func with_any_of_audience(audience: PoolStringArray) -> JWTVerifierBuilder: +func with_any_of_audience(audience: PackedStringArray) -> JWTVerifierBuilder: add_claim(JWTClaims.Public.AUDIENCE, audience) return self @@ -49,8 +49,8 @@ func accept_issued_at(leeway: int) -> JWTVerifierBuilder: with_issued_at(leeway) return self -func ignore_issued_at() -> JWTVerifierBuilder: - self.ignore_issued_at = true +func ignore_issued_at(v: bool = true) -> JWTVerifierBuilder: + self._ignore_issued_at = true return self func with_claim_presence(claim_name: String) -> JWTVerifierBuilder: @@ -64,9 +64,9 @@ func _add_leeway() -> void: claims[JWTClaims.Public.NOT_BEFORE] = self.leeway if (not claims.has(JWTClaims.Public.ISSUED_AT)): claims[JWTClaims.Public.ISSUED_AT] = self.leeway - if (ignore_issued_at): + if (_ignore_issued_at): claims.erase(JWTClaims.Public.ISSUED_AT) -func build(clock: int = OS.get_unix_time()) -> JWTVerifier: +func build(clock: int = int(Time.get_unix_time_from_system())) -> JWTVerifier: _add_leeway() return JWTVerifier.new(self.algorithm, self.claims, clock)