Skip to content

Instantly share code, notes, and snippets.

@jolle-c
Created April 25, 2017 07:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jolle-c/bf3ab3ee41bc1abebb0ce3d7b4c69ccd to your computer and use it in GitHub Desktop.
Save jolle-c/bf3ab3ee41bc1abebb0ce3d7b4c69ccd to your computer and use it in GitHub Desktop.
JSON Web Token (JWT) type for Lasso 9
[
/**!
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