Created
April 25, 2017 07:44
-
-
Save jolle-c/bf3ab3ee41bc1abebb0ce3d7b4c69ccd to your computer and use it in GitHub Desktop.
JSON Web Token (JWT) type for Lasso 9
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ | |
/**! | |
jwt | |
Lasso 9 type to sign, encode and verify JSON Web Tokens (JWT). | |
Requires a version of Lasso that supports json_encode and json_decode. | |
Developed and tested on Lasso 9.3. | |
Inspired by methods published by Alex Betz on Lasso Talk in April 2017 | |
2017-04-25 JC First version | |
Has the following methods | |
oncreate | |
Accepts these optional input params: | |
-key::string | |
The secret key used for signing and verifying a jwt. | |
-jwt::string | |
The jwt input when verifying. | |
-method::string | |
The algoritm used for signing and verifying. Note that the algoritm supplied | |
in the header is not used for signing. It is however compared to the supplied | |
algoritm and will create an error if they don't match. | |
Defaults to HS256. | |
-allowedmethods::array | |
An array with algoritms allowed for signing and verifying. Supplied -method | |
will be matched against the -allowedmethods. | |
Creates an error if no match is found. | |
Defaults to array('HS256'). | |
-acceptfields::map | |
Exta params used when verifying a jwt. Here's where you will put things like | |
expiry check, match against issuer etc. | |
Accepts the following params: | |
'iss' Issuer. A bytes object for case sensitive comparing. | |
If supplied will force a match with the payload param 'iss'. | |
'sub' Subject. A bytes object for case sensitive comparing. | |
If supplied will force a match with the payload param 'sub'. | |
'aud' Audience. Either a bytes object for case sensitive comparing or an array | |
with bytes objects. | |
If supplied will force a match with the payload param 'aud'. | |
'jti' JWT ID. A string representing a unique identifer for this JWT. | |
If supplied will force a match with the payload param 'jti'. | |
'verifyat' A date object or a date represented as an integer. Used for verifying | |
expiry and 'not before' values of the supplied jwt. If not supplied the present | |
datetime of the server will be used. | |
'gracePeriod' Seconds that the compare date times are allowed to skew from | |
requested time. If not set will default to 0 seconds. | |
The oncreate method does nothing more than store the input params for further use. | |
sign | |
Will return the signature for the provided input string using the method param. | |
The following methods/algoritms are supported: | |
'HS256', 'HS384', 'HS512' | |
Required input param: | |
msg::string | |
The text that is to be signed. | |
Optional input params: | |
key::string | |
The secret key used for signing. If not supplied will use the -key param | |
supplied in the oncreate method. | |
method::string | |
The algoritm used for signing. If not supplied will use the -method param | |
supplied in the oncreate method. | |
The signing will use Lassos encrypt_hmac method if the requested method is | |
supported by the Lasso version. This is OS dependent. If Lasso lacks support for | |
the requested chipher the code will attempt to call openssl using sys_process thru | |
the custom method shell. | |
verify | |
Confirms that the supplied jwt has a correct signature. Also extracts the header | |
and payload from the jwt and stores them for future access. | |
Will return boolean true if the signature is correct. If not returns boolean false | |
and stores the error in a data member that can be called using jwt -> error. | |
Has the following optional input params: | |
jwt::string | |
The jwt that is to be verified. If not supplied the param supplied in the | |
oncreate is used. | |
key::string | |
The secret key used for signing. If not supplied the -key param supplied | |
in the oncreate is used. | |
method::string | |
The algoritm used for signing. If not supplied the -method param | |
supplied in the oncreate method is used. | |
allowedmethods::array | |
An array with algoritms allowed for signing and verifying. Supplied -method | |
will be matched against the -allowedmethods. If not supplied the | |
-allowedmethods param supplied in the oncreate method is used. | |
acceptfields::map | |
Exta params used when verifying the jwt. If not supplied the -acceptfields | |
param supplied in the oncreate method is used. | |
encode | |
Creates a jwt using the provided payload. Will return the complete jwt as a string. | |
Adds a header to the jwt with the following content: | |
("typ" = "JWT", "alg" = [method]) | |
Required input param: | |
payload::any | |
Probably a map but can be anything that can be json encoded. | |
Optional input params: | |
key::string | |
The secret key used for signing. If not supplied the -key param supplied | |
in the oncreate is used. | |
method::string | |
The algoritm used for signing. If not supplied the -method param | |
supplied in the oncreate method is used. | |
payload | |
Will return the payload of the provided jwt. If jwt -> payload is called before | |
jwt -> verify then the verify method will run before returning the payload. | |
header | |
Will return the header of the provided jwt. If jwt -> header is called before | |
jwt -> verify then the verify method will run before returning the header. | |
error | |
If jwt -> verify returns false then jwt -> error will return the error messages. | |
The type has the following public data members | |
jwt::string | |
Holds the input jwt string consisting of three parts, header, payload and signature. | |
encmethod::string | |
The requested algoritm used for signing and verify jwts. | |
allowedmethods::array | |
An array with algoritms allowed for signing and verifying. | |
acceptfields::map | |
Exta params used when verifying a jwt. | |
In addition to the built in methods the type relies on the following methods originally | |
authored by Alex Betz. | |
urlsafeB64Decode | |
urlsafeB64Encode | |
stringToUrlSafe | |
These are supplied after the jwt type. | |
If Lassos encrypt_hmac method lacks support for the requested chipher the code will | |
attempt to call openssl using sys_process thru the custom method shell. | |
Thus shell needs to be installed. | |
Shell can be found here: | |
https://gist.github.com/jolle-c/7e3a6a0d30a032573bb67eae423ff865 | |
It is the developers responsibility that openssl is installed, accessible for Lasso and | |
supports the requested chiphers | |
EXAMPLES | |
local( | |
expirydate = date, | |
nbfdate = date, | |
sub = lasso_uniqueid | |
) | |
#expirydate -> add(-hour = 2) | |
#nbfdate -> add(-minute = 10) | |
local(payload = map( | |
'admin' = true, | |
'name' = 'John Doe', | |
'sub' = #sub, | |
'exp' = #expirydate -> asinteger, | |
'ist' = date -> asinteger, | |
'nbf' = #nbfdate -> asinteger, | |
'iss' = 'https://mysite.com' | |
) ) | |
local(encoded = jwt -> encode(#payload, 'top secret', 'HS256')) | |
#encoded | |
'<hr />' | |
local(verifydate = date) | |
#verifydate -> add(-hour = 1) | |
local( | |
jwt1 = jwt( | |
-jwt = #encoded, | |
-key = 'top secret', | |
-acceptfields = map('iss' = bytes('https://mysite.com'), 'verifyat' = #verifydate) | |
) | |
) | |
#jwt1 -> verify? #jwt1 -> payload + '<br />' + #jwt1 -> header | #jwt1 -> error | |
'<hr />' | |
local( | |
jwt2 = jwt( | |
-jwt = #encoded, | |
-key = 'top secret', | |
-acceptfields = map('iss' = bytes('https://othersite.com'), 'verifyat' = #verifydate) | |
) | |
) | |
#jwt2 -> verify? #jwt2 -> payload + '<br>' + #jwt2 -> header | #jwt2 -> error | |
'<hr />' | |
#verifydate -> add(-hour = 3) | |
local( | |
jwt2 = jwt( | |
-jwt = #encoded, | |
-key = 'top secret', | |
-acceptfields = map('iss' = bytes('https://mysite.com'), 'verifyat' = #verifydate) | |
) | |
) | |
#jwt2 -> verify? #jwt2 -> payload + '<br>' + #jwt2 -> header | #jwt2 -> error | |
*/ | |
define jwt => type { | |
data private key::string, | |
public jwt::string, | |
public encmethod::string, | |
public allowedmethods::array, | |
public acceptfields::map = map, | |
private verified::boolean = false, | |
private header_container, | |
private payload_container, | |
private signature, | |
private errors::array = array, | |
private verify_ran::boolean = false | |
public oncreate( | |
-key::string = '', | |
-jwt::string = '', | |
-method::string = 'HS256', | |
-allowedmethods::array = array('HS256'), | |
-acceptfields::map = map | |
) => { | |
.key = #key | |
.jwt = #jwt | |
.encmethod = #method | |
.allowedmethods = #allowedmethods | |
.acceptfields = #acceptfields | |
} | |
public sign( | |
msg::string, | |
key::string = .key, | |
method::string = .encmethod | |
) => { | |
.key = #key | |
local( | |
methods = map('HS256' = (:'-sha256', 'SHA256'), 'HS384' = (:'-sha384', 'SHA384'), 'HS512' = (:'-sha512', 'SHA512')), | |
// methods = map('HS256' = (:'HmacSHA256', 'SHA256'), 'HS384' = (:'HmacSHA384', 'SHA384'), 'HS512' = (:'HmacSHA512', 'SHA512')), // for use with LJAPI kin_hmac_sha | |
method_used = #methods -> find(#method), | |
) | |
fail_if(not #method_used -> isa(::staticarray), -1, 'JWT sign: Supplied encryption method not supported') | |
if(cipher_list(-digest) >> #method_used -> last) => { | |
return encrypt_hmac( | |
-token = #msg, | |
-password = #key, | |
-digest = #method_used -> last, | |
-base64 | |
) | |
else | |
// using sys_process via shell, calling openssl | |
local(syntax = 'echo -n ' + #msg + ' | openssl dgst -binary ' + | |
#method_used -> first + ' -hmac "' + #key + '" | openssl base64 -a') | |
return string(shell(#syntax)) | |
// return kin_hmac_sha(#key, #msg, #method_used -> first) | |
} | |
} | |
public verify( | |
jwt::string = .jwt, | |
key::string = .key, | |
method::string = .encmethod, | |
allowedmethods::array = .allowedmethods, | |
acceptfields::map = .acceptfields | |
) => { | |
.key = #key | |
.jwt = #jwt | |
.encmethod = #method | |
.allowedmethods = #allowedmethods | |
.acceptfields = #acceptfields | |
.verify_ran = true | |
local( | |
jwt_alg::string = string, | |
parts = #jwt -> split('.'), | |
partsize = #parts -> size, | |
iss_required = .acceptfields -> find('iss') or array, | |
sub_required = .acceptfields -> find('sub') or array, | |
aud_required = .acceptfields -> find('aud') or array, | |
jti_required = .acceptfields -> find('jti') or string, | |
verifyat = (.acceptfields -> find('verifyat') or date) -> asinteger, | |
gracePeriod = (.acceptfields -> find('gracePeriod')) -> asinteger, | |
headb64, bodyb64, cryptob64, | |
iss_provided, sub_provided, aud_provided, exp_provided, nbf_provided, | |
iat_provided, jti_provided | |
) | |
if(#partsize > 1) => { | |
#headb64 = #parts -> get(1) | |
#bodyb64 = #parts -> get(2) | |
.header_container = json_decode(urlsafeB64Decode(#headb64) -> asstring) or map | |
.payload_container = json_decode(urlsafeB64Decode(#bodyb64) -> asstring) | |
#jwt_alg = .header_container -> find('alg') or string | |
} | |
if(not(#jwt_alg) or not(#jwt_alg == .encmethod)) => { | |
.errors -> insert('JWT verify error: Signature algoritm mismatch (' + #jwt_alg + ')') | |
.verified = false | |
return false | |
} | |
if(not (.allowedmethods >> .encmethod)) => { | |
.errors -> insert('JWT verify error: Signature algoritm is not allowed (' + .encmethod + ')') | |
.verified = false | |
return false | |
} | |
if(.payload_container -> isa(::map)) => { | |
#iss_provided = .payload_container -> find('iss') | |
#sub_provided = .payload_container -> find('sub') | |
#aud_provided = .payload_container -> find('aud') or string | |
#exp_provided = .payload_container -> find('exp') | |
#nbf_provided = .payload_container -> find('nbf') | |
#iat_provided = .payload_container -> find('iat') | |
#jti_provided = .payload_container -> find('jti') | |
} | |
if(#iss_required -> size and not(#iss_required >> bytes(#iss_provided))) => { | |
.errors -> insert('JWT verify error: Issuer is not allowed (' + #iss_provided + ')') | |
.verified = false | |
return false | |
} | |
if(#sub_required -> size and not(#sub_required >> bytes(#sub_provided))) => { | |
.errors -> insert('JWT verify error: Subject is not allowed (' + #sub_provided + ')') | |
.verified = false | |
return false | |
} | |
if(#aud_required -> size) => { | |
local(found = false) | |
#aud_provided -> isa(::string) ? #aud_provided = array(#aud_provided) | |
with ap in #aud_provided do { | |
if(not #found) => { | |
#aud_required >> bytes(#ap) ? #found = true | |
} | |
} | |
if(not #found) => { | |
.errors -> insert('JWT verify error: Audience is not allowed (' + #aud_provided + ')') | |
.verified = false | |
return false | |
} | |
} | |
if(#jti_required -> size and not(#jti_required == #jti_provided)) => { | |
.errors -> insert('JWT verify error: JTI mismatch (' + #jti_provided + ')') | |
.verified = false | |
return false | |
} | |
if(#exp_provided and not(#verifyat < (#exp_provided -> asinteger + #graceperiod))) => { | |
//Payload.exp (expire) - Validation time is smaller than Payload.exp + gracePeriod. | |
.errors -> insert('JWT verify error: Token has expired (' + #exp_provided + ')') | |
.verified = false | |
return false | |
} | |
if(#nbf_provided and not(#verifyat > (#nbf_provided -> asinteger - #graceperiod))) => { | |
//Payload.nbf (not before) - Validation time is greater than Payload.nbf - gracePeriod. | |
.errors -> insert('JWT verify error: Token did not meet Not Before criteria (' + #nbf_provided + ')') | |
.verified = false | |
return false | |
} | |
if(#iat_provided and not(#verifyat > (#iat_provided -> asinteger - #graceperiod))) => { | |
//Payload.iat (issued at) - Validation time is greater than Payload.iat - gracePeriod. | |
.errors -> insert('JWT verify error: Token Issued At not correct (' + #iat_provided + ')') | |
.verified = false | |
return false | |
} | |
if(#partsize > 2) => { | |
#cryptob64 = #parts -> get(3) | |
.signature = stringToUrlSafe(.sign(#headb64 + '.' + #bodyb64, #key, .encmethod)) | |
if(#cryptob64 == .signature) => { | |
.verified = true | |
else | |
.errors -> insert('JWT verify error: Signature mismatch') | |
.verified = false | |
return false | |
} | |
else(.encmethod == 'none') | |
.verified = true | |
} | |
not(.verified) ? .errors -> insert('JWT verify error: Unknown error') | |
return .verified | |
} | |
public encode( | |
payload, | |
key::string = .key, | |
method::string = .encmethod | |
) => { | |
.payload_container = #payload | |
.key = #key | |
.encmethod = #method | |
.header_container = map("typ" = "JWT", "alg" = .encmethod) | |
local( | |
headb64 = urlsafeB64Encode(json_encode(.header_container)), | |
bodyb64 = urlsafeB64Encode(json_encode(.payload_container)), | |
cryptob64 = (.encmethod == 'none' ? string | stringToUrlSafe(.sign(#headb64 + '.' + #bodyb64))) | |
) | |
return #headb64 + '.' + #bodyb64 + '.' + #cryptob64 | |
} | |
public payload() => { | |
not(.verify_ran) ? .verify | |
return .payload_container | |
} | |
public header() => { | |
not(.verify_ran) ? .verify | |
return .header_container | |
} | |
public error => { | |
not(.errors -> size) ? return 'No error' | |
return .errors -> join('\n') | |
} | |
} | |
define urlsafeB64Decode(input::string) => { | |
local( | |
_input = string(#input) // copy as to not tamper with original string | |
) | |
#_input -> append('=' * ( 4 - #_input -> size % 4)) | |
#_input -> replace('-', '+') | |
return(bytes(#_input)->decodebase64) | |
} | |
define urlsafeB64Encode(input::string) => stringToUrlSafe(string(bytes(#input) -> encodebase64)) | |
define stringToUrlSafe(input::string) => { | |
local( | |
_input = string(#input) // copy as to not tamper with original string | |
) | |
#_input -> replace('=', '') | |
#_input -> replace('+', '-') | |
#_input -> replace('/', '_') | |
return(#_input) | |
} | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment