Skip to content

Commit

Permalink
Switched to Godot 4 support, Repaired broken RS256 implementation. (#10)
Browse files Browse the repository at this point in the history
* Fixed RS256, Switched to Godot 4 Syntax, Updated README

* random_secret() and JWTBuilder.neW() now respects the passed parameters

* Switch from tabs to spaces

* Switch from tabs to spaces
  • Loading branch information
crazychenz authored Jan 4, 2022
1 parent 632e4a1 commit 91d2473
Show file tree
Hide file tree
Showing 13 changed files with 156 additions and 84 deletions.
62 changes: 47 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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 = "<a jwt>"
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 = "<your secret token>"
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("<your private key PEM string>", false)
public_key.load_from_string("<your public key PEM string>", true)
var jwt: String = "<a jwt>"
var secret: String = "<your secret token>"
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 = "<a jwt>"
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
Expand Down
2 changes: 1 addition & 1 deletion addons/jwt/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="JWT"
description=""
author="Nicolò (fenix-hub) Santilio"
version="1.5"
version="1.0"
script="plugin.gd"
2 changes: 1 addition & 1 deletion addons/jwt/plugin.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
tool
@tool
extends EditorPlugin


Expand Down
2 changes: 1 addition & 1 deletion addons/jwt/src/JWT.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extends Reference
extends RefCounted
class_name JWT

static func create(algorithm: JWTAlgorithm = null, header_claims: Dictionary = {}, payload_claims: Dictionary = {}) -> JWTBuilder:
Expand Down
49 changes: 33 additions & 16 deletions addons/jwt/src/JWTAlgorithm.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extends Reference
extends RefCounted
class_name JWTAlgorithm

enum Type {
Expand All @@ -7,38 +7,55 @@ enum Type {
RSA256
}

var _hash: int = -1
var _alg: int = -1
var _secret: String = ""

var crypto: Crypto = Crypto.new()
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)
31 changes: 25 additions & 6 deletions addons/jwt/src/JWTAlgorithmBuilder.gd
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 2 additions & 2 deletions addons/jwt/src/JWTBaseBuilder.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extends Reference
extends RefCounted
class_name JWTBaseBuilder

func with_header(header_claims: Dictionary) -> JWTBaseBuilder:
Expand All @@ -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

Expand Down
18 changes: 11 additions & 7 deletions addons/jwt/src/JWTBuilder.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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]
2 changes: 1 addition & 1 deletion addons/jwt/src/JWTClaims.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extends Reference
extends RefCounted
class_name JWTClaims


Expand Down
18 changes: 9 additions & 9 deletions addons/jwt/src/JWTDecoder.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extends Reference
extends RefCounted
class_name JWTDecoder

var parts: Array = []
Expand All @@ -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")
Expand All @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions addons/jwt/src/JWTUtils.gd
Original file line number Diff line number Diff line change
@@ -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("-","+"))
Loading

0 comments on commit 91d2473

Please sign in to comment.