diff --git a/readme.md b/readme.md index 911471e..580a2fe 100644 --- a/readme.md +++ b/readme.md @@ -4,21 +4,31 @@ A simple, self-contained module for making and verifying JWTs using HMAC SHA256. ## Example ```v import jwt -import json -const secret := "secret-key" +const secret = 'secret-key' -// Create a new token -payload := jwt.Payload{ - sub: "1234567890", - ext: json.encode(/* some struct */) +pub struct Credential { + user string + pass string } -token := jwt.Token.new(payload, secret) -respond_with(token.str()) -// Validate a token from the web -token := jwt.Token.from_str(/* receive a token from the web */) -if token.valid(secret) { - // do stuff +fn main() { + // Create a new token + payload := jwt.Payload[Credential]{ + sub: '1234567890' + ext: Credential{ + user: 'splashsky' + pass: 'password' + } + } + token := jwt.Token.new(payload, secret) + receive_token_from_web := token.str() + + // Validate a token from the web + obj_token := jwt.from_str[Credential](receive_token_from_web)! + if obj_token.valid(secret) { + println('token valid!') + dump(obj_token.payload.ext) + } } ``` diff --git a/src/header.v b/src/header.v index 989bc85..1baf899 100644 --- a/src/header.v +++ b/src/header.v @@ -1,6 +1,6 @@ module jwt pub struct Header { - alg string = "HS256" - typ string = "JWT" + alg string = 'HS256' + typ string = 'JWT' } diff --git a/src/jwt.v b/src/jwt.v index cc7b675..564ab5b 100644 --- a/src/jwt.v +++ b/src/jwt.v @@ -1,75 +1,66 @@ module jwt import encoding.base64 -import x.json2 as json +import json import crypto.hmac import crypto.sha256 import time -pub struct Token { - header string - payload string +pub struct Token[T] { + header string signature string +pub: + payload Payload[T] } -pub fn Token.new(payload Payload, secret string) Token { - header := base64.url_encode(json.encode[Header](Header{}).bytes()) - payload_string := base64.url_encode(json.encode[Payload](payload).bytes()) - signature := base64.url_encode(hmac.new( - secret.bytes(), - "${header}.${payload_string}".bytes(), - sha256.sum, - sha256.block_size - ).bytestr().bytes()) +pub fn Token.new[T](payload Payload[T], secret string) Token[T] { + header := base64.url_encode(json.encode(Header{}).bytes()) + payload_string := base64.url_encode(json.encode(payload).bytes()) - return Token{ - header: header, - payload: payload_string, - signature: signature, + signature := base64.url_encode(hmac.new(secret.bytes(), '${header}.${payload_string}'.bytes(), + sha256.sum, sha256.block_size).bytestr().bytes()) + + return Token[T]{ + header: header + payload: payload + signature: signature } } -pub fn Token.from_str(token string) !Token { - parts := token.split(".") +pub fn from_str[T](token string) !Token[T] { + parts := token.split('.') if parts.len != 3 { - return error("Invalid token") + return error('Invalid token') } - return Token{ - header: parts[0], - payload: parts[1], - signature: parts[2], + return Token[T]{ + header: parts[0] + payload: json.decode(Payload[T], base64.url_decode_str(parts[1]))! + signature: parts[2] } } -pub fn (t Token) str() string { - return t.header + "." + t.payload + "." + t.signature +pub fn (t Token[T]) str() string { + payload := base64.url_encode(json.encode(t.payload).bytes()) + return t.header + '.' + payload + '.' + t.signature } -pub fn (t Token) valid(secret string) bool { +pub fn (t Token[T]) valid(secret string) bool { if t.expired() { return false } - parts := t.str().split(".") + parts := t.str().split('.') if parts.len != 3 { return false } - expected_signature := base64.url_encode(hmac.new( - secret.bytes(), - "${parts[0]}.${parts[1]}".bytes(), // header + payload - sha256.sum, - sha256.block_size - ).bytestr().bytes()) + expected_signature := base64.url_encode(hmac.new(secret.bytes(), '${parts[0]}.${parts[1]}'.bytes(), // header + payload + sha256.sum, sha256.block_size).bytestr().bytes()) return parts[2] == expected_signature // signature == expected_signature } -pub fn (t Token) payload() !Payload { - return json.decode[Payload](base64.url_decode_str(t.payload))! -} - -pub fn (t Token) expired() bool { - return t.payload() or { return false }.exp or { return false } < time.now() +pub fn (t Token[T]) expired() bool { + return t.payload.exp.time() or { return false } < time.now() } diff --git a/src/jwt_test.v b/src/jwt_test.v index 2656297..952d7c0 100644 --- a/src/jwt_test.v +++ b/src/jwt_test.v @@ -1,46 +1,71 @@ module jwt -import x.json2 as json import time -const secret := "secret" -const claims := {"name": "John Doe", "admin": "true"} +const no_secret = 'pass secret' +const secret = 'secret' +const claims = { + 'name': 'John Doe' + 'admin': 'true' +} fn test_valid() { - payload := Payload{ - sub: "1234567890", - ext: json.encode(claims) + payload := Payload[map[string]string]{ + sub: '1234567890' + ext: jwt.claims + } + token := Token.new(payload, jwt.secret) + + assert token.valid(jwt.secret) +} + +fn test_invalid() { + payload := Payload[map[string]string]{ + sub: '1234567890' + ext: jwt.claims } - token := Token.new(payload, secret) + token := Token.new(payload, jwt.secret) - assert token.valid(secret) == true + assert !token.valid(jwt.no_secret) } fn test_expired() { - payload := Payload{ - sub: "1234567890", - ext: json.encode(claims), - exp: time.parse("2019-01-01 00:00:00") or { panic(err) } + payload := Payload[map[string]string]{ + sub: '1234567890' + ext: jwt.claims + exp: '2019-01-01 00:00:00' + } + token := Token.new(payload, jwt.secret) + + assert !token.valid(jwt.secret) + assert token.expired() +} + +fn test_no_expired() { + payload := Payload[map[string]string]{ + sub: '1234567890' + ext: jwt.claims + exp: time.now().add_seconds(10).str() } - token := Token.new(payload, secret) + token := Token.new(payload, jwt.secret) - assert token.expired() == true - assert token.valid(secret) == false + assert token.valid(jwt.secret) + assert !token.expired() } fn test_from_str() { - payload := Payload{ - sub: "1234567890", - ext: json.encode(claims) + payload := Payload[map[string]string]{ + sub: '1234567890' + ext: jwt.claims } - payload2 := Payload{ - sub: "0987654321", - ext: json.encode(claims) + payload2 := Payload[map[string]string]{ + sub: '0987654321' + ext: jwt.claims } - token := Token.new(payload, secret) - token2 := Token.from_str(token.str())! - token3 := Token.new(payload2, secret) + token := Token.new(payload, jwt.secret) + token2 := from_str[map[string]string](token.str())! //Não está funcionando + token3 := Token.new(payload2, jwt.secret) assert token2 == token assert token2 != token3 diff --git a/src/payload.v b/src/payload.v index 8d7f146..512a07f 100644 --- a/src/payload.v +++ b/src/payload.v @@ -2,11 +2,18 @@ module jwt import time -pub struct Payload { +type JsTime = string + +pub struct Payload[T] { +pub: iss ?string @[omitempty] sub ?string @[omitempty] aud ?string @[omitempty] - exp ?time.Time @[omitempty] - iat ?time.Time @[omitempty] - ext ?string @[omitempty] + exp JsTime @[omitempty] + iat JsTime @[omitempty] + ext T @[omitempty] +} + +pub fn (jst JsTime) time() ?time.Time { + return time.parse(jst) or { return none } }