diff --git a/README.md b/README.md index b8755b8..01324cf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +https://github.com/zendesk/classic_asp_jwt.git + ## Classic ASP JWT A JWT implementation in Classic ASP, currently only supports `JWTEncode(dictionary, secret)`. @@ -7,7 +9,7 @@ A JWT implementation in Classic ASP, currently only supports `JWTEncode(dictiona ```asp <% -Dim sKey, dAttributes, sToken +Dim sKey, dAttributes, sToken, decodedPayload, isValidJWT sKey = "Shared Secret" Set dAttributes=Server.CreateObject("Scripting.Dictionary") @@ -19,6 +21,12 @@ dAttributes.Add "name", "Roger" dAttributes.Add "email", "roger@example.com" sToken = JWTEncode(dAttributes, sKey) + +' Decode JWT token string and get payload. (WARNING : Not verify) +decodedPayload = JWTDecode(sToken) + +' Verify JWT String. (Returns Boolean) +isValidJWT = JWTVerify(sToken, sKey) %> ``` diff --git a/external/aspJSON.asp b/external/aspJSON.asp index b91ddca..799e52f 100644 --- a/external/aspJSON.asp +++ b/external/aspJSON.asp @@ -1,113 +1,135 @@ <% -'July 2012 - Version 1.0 by Gerrit van Kuipers - http://www.aspjson.com/ +'January 2021 - Version 1.1 by Gerrit van Kuipers Class aspJSON Public data Private p_JSONstring - Private p_datatype + Private aj_in_string, aj_in_escape, aj_i_tmp, aj_char_tmp, aj_s_tmp, aj_line_tmp, aj_line, aj_lines, aj_currentlevel, aj_currentkey, aj_currentvalue, aj_newlabel, aj_XmlHttp, aj_RegExp, aj_colonfound Private Sub Class_Initialize() Set data = Collection() - p_datatype = "{}" + + Set aj_RegExp = New regexp + aj_RegExp.Pattern = "\s{0,}(\S{1}[\s,\S]*\S{1})\s{0,}" + aj_RegExp.Global = False + aj_RegExp.IgnoreCase = True + aj_RegExp.Multiline = True End Sub Private Sub Class_Terminate() Set data = Nothing + Set aj_RegExp = Nothing End Sub - Public Function loadJSON(strInput) - p_JSONstring = CleanUpJSONstring(Trim(strInput)) - lines = Split(p_JSONstring, vbCrLf) + Public Sub loadJSON(inputsource) + inputsource = aj_MultilineTrim(inputsource) + If Len(inputsource) = 0 Then Err.Raise 1, "loadJSON Error", "No data to load." + + Select Case Left(inputsource, 1) + Case "{", "[" + Case Else + Set aj_XmlHttp = Server.CreateObject("Msxml2.ServerXMLHTTP") + aj_XmlHttp.open "POST", inputsource, False + aj_XmlHttp.setRequestHeader "Content-Type", "text/json" + aj_XmlHttp.setRequestHeader "CharSet", "UTF-8" + aj_XmlHttp.Send + inputsource = aj_XmlHttp.responseText + Set aj_XmlHttp = Nothing + End Select + + p_JSONstring = CleanUpJSONstring(inputsource) + aj_lines = Split(p_JSONstring, Chr(13) & Chr(10)) Dim level(99) - currentlevel = 1 - Set level(currentlevel) = data - For Each line In lines - currentkey = "" - currentvalue = "" - If Instr(line, ":") > 0 Then - '"created":"2010-04-30 09:20:09" - - in_string = False - in_escape = False - For i_tmp = 1 To Len(line) - If in_escape Then - in_escape = False + aj_currentlevel = 1 + Set level(aj_currentlevel) = data + For Each aj_line In aj_lines + aj_currentkey = "" + aj_currentvalue = "" + If Instr(aj_line, ":") > 0 Then + aj_in_string = False + aj_in_escape = False + aj_colonfound = False + For aj_i_tmp = 1 To Len(aj_line) + If aj_in_escape Then + aj_in_escape = False Else - char = Mid(line, i_tmp, 1) - Select Case char + Select Case Mid(aj_line, aj_i_tmp, 1) Case """" - in_string = Not in_string + aj_in_string = Not aj_in_string Case ":" - If Not in_escape Then - currentkey = Left(line, i_tmp - 1) - currentvalue = Mid(line, i_tmp + 1) + If Not aj_in_escape And Not aj_in_string Then + aj_currentkey = Left(aj_line, aj_i_tmp - 1) + aj_currentvalue = Mid(aj_line, aj_i_tmp + 1) + aj_colonfound = True Exit For End If Case "\" - in_escape = True + aj_in_escape = True End Select End If Next - currentkey = Strip(JSONDecode(currentkey), """") - If Not level(currentlevel).exists(currentkey) Then level(currentlevel).Add currentkey, "" + if aj_colonfound then + aj_currentkey = aj_Strip(aj_JSONDecode(aj_currentkey), """") + If Not level(aj_currentlevel).exists(aj_currentkey) Then level(aj_currentlevel).Add aj_currentkey, "" + end if End If - If Instr(line,"{") > 0 Or Instr(line,"[") > 0 Then - If Len(currentkey) = 0 Then currentkey = level(currentlevel).Count - Set level(currentlevel).Item(currentkey) = Collection() - Set level(currentlevel + 1) = level(currentlevel).Item(currentkey) - currentlevel = currentlevel + 1 - currentkey = "" - ElseIf Instr(line,"}") > 0 Or Instr(line,"]") > 0 Then - currentlevel = currentlevel - 1 - ElseIf Len(Trim(line)) > 0 Then - if Len(currentvalue) = 0 Then currentvalue = getJSONValue(line) - currentvalue = getJSONValue(currentvalue) - - If Len(currentkey) = 0 Then currentkey = level(currentlevel).Count - level(currentlevel).Item(currentkey) = currentvalue + If right(aj_line,1) = "{" Or right(aj_line,1) = "[" Then + If Len(aj_currentkey) = 0 Then aj_currentkey = level(aj_currentlevel).Count + Set level(aj_currentlevel).Item(aj_currentkey) = Collection() + Set level(aj_currentlevel + 1) = level(aj_currentlevel).Item(aj_currentkey) + aj_currentlevel = aj_currentlevel + 1 + aj_currentkey = "" + ElseIf right(aj_line,1) = "}" Or right(aj_line,1) = "]" or right(aj_line,2) = "}," Or right(aj_line,2) = "]," Then + aj_currentlevel = aj_currentlevel - 1 + ElseIf Len(Trim(aj_line)) > 0 Then + If Len(aj_currentvalue) = 0 Then aj_currentvalue = aj_line + aj_currentvalue = getJSONValue(aj_currentvalue) + + If Len(aj_currentkey) = 0 Then aj_currentkey = level(aj_currentlevel).Count + level(aj_currentlevel).Item(aj_currentkey) = aj_currentvalue End If Next - End Function + End Sub Public Function Collection() - set Collection = Server.CreateObject("Scripting.Dictionary") + Set Collection = Server.CreateObject("Scripting.Dictionary") End Function Public Function AddToCollection(dictobj) - if TypeName(dictobj) <> "Dictionary" then Err.Raise 1, "AddToCollection Error", "Not a collection." - newlabel = dictobj.Count - dictobj.Add newlabel, Collection() - set AddToCollection = dictobj.item(newlabel) + If TypeName(dictobj) <> "Dictionary" Then Err.Raise 1, "AddToCollection Error", "Not a collection." + aj_newlabel = dictobj.Count + dictobj.Add aj_newlabel, Collection() + Set AddToCollection = dictobj.item(aj_newlabel) end function - Private Function CleanUpJSONstring(originalstring) - originalstring = Replace(originalstring,vbCrLf, "") - - p_datatype = Left(originalstring, 1) & Right(originalstring, 1) - originalstring = Mid(originalstring, 2, Len(originalstring) - 2) - in_string = False : in_escape = False - For i_tmp = 1 To Len(originalstring) - If in_escape Then - in_escape = False + Private Function CleanUpJSONstring(aj_originalstring) + aj_originalstring = Replace(aj_originalstring, Chr(13) & Chr(10), "") + aj_originalstring = Mid(aj_originalstring, 2, Len(aj_originalstring) - 2) + aj_in_string = False : aj_in_escape = False : aj_s_tmp = "" + For aj_i_tmp = 1 To Len(aj_originalstring) + aj_char_tmp = Mid(aj_originalstring, aj_i_tmp, 1) + If aj_in_escape Then + aj_in_escape = False + aj_s_tmp = aj_s_tmp & aj_char_tmp Else - char_tmp = Mid(originalstring, i_tmp, 1) - Select Case char_tmp - Case "\" : in_escape = True - Case """" : s_tmp = s_tmp & char_tmp : in_string = Not in_string + Select Case aj_char_tmp + Case "\" : aj_s_tmp = aj_s_tmp & aj_char_tmp : aj_in_escape = True + Case """" : aj_s_tmp = aj_s_tmp & aj_char_tmp : aj_in_string = Not aj_in_string Case "{", "[" - s_tmp = s_tmp & char_tmp & InlineIf(in_string, "", vbCrLf) + aj_s_tmp = aj_s_tmp & aj_char_tmp & aj_InlineIf(aj_in_string, "", Chr(13) & Chr(10)) Case "}", "]" - s_tmp = s_tmp & InlineIf(in_string, "", vbCrLf) & char_tmp - Case "," : s_tmp = s_tmp & char_tmp & InlineIf(in_string, "", vbCrLf) - Case Else : s_tmp = s_tmp & char_tmp + aj_s_tmp = aj_s_tmp & aj_InlineIf(aj_in_string, "", Chr(13) & Chr(10)) & aj_char_tmp + Case "," : aj_s_tmp = aj_s_tmp & aj_char_tmp & aj_InlineIf(aj_in_string, "", Chr(13) & Chr(10)) + Case Else : aj_s_tmp = aj_s_tmp & aj_char_tmp End Select End If Next CleanUpJSONstring = "" - s_tmp = split(s_tmp, vbCrLf) - For Each line_tmp In s_tmp - CleanUpJSONstring = CleanUpJSONstring & Trim(line_tmp) & vbCrLf + aj_s_tmp = Split(aj_s_tmp, Chr(13) & Chr(10)) + For Each aj_line_tmp In aj_s_tmp + aj_line_tmp = Replace(Replace(aj_line_tmp, Chr(10), ""), Chr(13), "") + CleanUpJSONstring = CleanUpJSONstring & aj_Trim(aj_line_tmp) & Chr(13) & Chr(10) Next End Function @@ -124,53 +146,58 @@ Class aspJSON Case Else If (Instr(val, """") = 0) Then If IsNumeric(val) Then - getJSONValue = CDbl(val) + getJSONValue = aj_ReadNumericValue(val) Else getJSONValue = val End If Else If Left(val,1) = """" Then val = Mid(val, 2) If Right(val,1) = """" Then val = Left(val, Len(val) - 1) - getJSONValue = JSONDecode(Trim(val)) + getJSONValue = aj_JSONDecode(Trim(val)) End If End Select End Function Private JSONoutput_level Public Function JSONoutput() + Dim wrap_dicttype, aj_label JSONoutput_level = 1 - JSONoutput = Left(p_datatype, 1) & vbCrLf & GetDict(data) & Right(p_datatype, 1) + wrap_dicttype = "[]" + For Each aj_label In data + If Not aj_IsInt(aj_label) Then wrap_dicttype = "{}" + Next + JSONoutput = Left(wrap_dicttype, 1) & Chr(13) & Chr(10) & GetDict(data) & Right(wrap_dicttype, 1) End Function Private Function GetDict(objDict) - For Each item In objDict - Select Case TypeName(objDict.Item(item)) + Dim aj_item, aj_keyvals, aj_label, aj_dicttype + For Each aj_item In objDict + Select Case TypeName(objDict.Item(aj_item)) Case "Dictionary" GetDict = GetDict & Space(JSONoutput_level * 4) - - dicttype = "[]" - For Each label In objDict.Item(item).Keys - If Not IsInt(label) Then dicttype = "{}" + + aj_dicttype = "[]" + For Each aj_label In objDict.Item(aj_item).Keys + If Not aj_IsInt(aj_label) Then aj_dicttype = "{}" Next - - If IsInt(item) Then - GetDict = GetDict & Left(dicttype,1) & vbCrLf + If aj_IsInt(aj_item) Then + GetDict = GetDict & (Left(aj_dicttype,1) & Chr(13) & Chr(10)) Else - GetDict = GetDict & """" & JSONEncode(item) & """" & ": " & Left(dicttype,1) & vbCrLf + GetDict = GetDict & ("""" & aj_JSONEncode(aj_item) & """" & ": " & Left(aj_dicttype,1) & Chr(13) & Chr(10)) End If JSONoutput_level = JSONoutput_level + 1 - - keyvals = objDict.Keys - GetDict = GetDict & GetSubDict(objDict.Item(item)) & Space(JSONoutput_level * 4) & Right(dicttype,1) & InlineIf(item = keyvals(objDict.Count - 1),"" , ",") & vbCrLf + + aj_keyvals = objDict.Keys + GetDict = GetDict & (GetSubDict(objDict.Item(aj_item)) & Space(JSONoutput_level * 4) & Right(aj_dicttype,1) & aj_InlineIf(aj_item = aj_keyvals(objDict.Count - 1),"" , ",") & Chr(13) & Chr(10)) Case Else - keyvals = objDict.Keys - GetDict = GetDict & Space(JSONoutput_level * 4) & InlineIf(IsInt(item), "", """" & JSONEncode(item) & """: ") & WriteValue(objDict.Item(item)) & InlineIf(item = keyvals(objDict.Count - 1),"" , ",") & vbCrLf + aj_keyvals = objDict.Keys + GetDict = GetDict & (Space(JSONoutput_level * 4) & aj_InlineIf(aj_IsInt(aj_item), "", """" & aj_JSONEncode(aj_item) & """: ") & WriteValue(objDict.Item(aj_item)) & aj_InlineIf(aj_item = aj_keyvals(objDict.Count - 1),"" , ",") & Chr(13) & Chr(10)) End Select Next End Function - Private Function IsInt(val) - IsInt = (TypeName(val) = "Integer" Or TypeName(val) = "Long") + Private Function aj_IsInt(val) + aj_IsInt = (TypeName(val) = "Integer" Or TypeName(val) = "Long") End Function Private Function GetSubDict(objSubDict) @@ -180,14 +207,14 @@ Class aspJSON Private Function WriteValue(ByVal val) Select Case TypeName(val) - Case "Double", "Integer", "Long": WriteValue = val + Case "Double", "Integer", "Long": WriteValue = replace(val, ",", ".") Case "Null" : WriteValue = "null" - Case "Boolean" : WriteValue = InlineIf(val, "true", "false") - Case Else : WriteValue = """" & JSONEncode(val) & """" + Case "Boolean" : WriteValue = aj_InlineIf(val, "true", "false") + Case Else : WriteValue = """" & aj_JSONEncode(val) & """" End Select End Function - Private Function JSONEncode(ByVal val) + Private Function aj_JSONEncode(ByVal val) val = Replace(val, "\", "\\") val = Replace(val, """", "\""") 'val = Replace(val, "/", "\/") @@ -196,10 +223,10 @@ Class aspJSON val = Replace(val, Chr(10), "\n") val = Replace(val, Chr(13), "\r") val = Replace(val, Chr(9), "\t") - JSONEncode = Trim(val) + aj_JSONEncode = Trim(val) End Function - Private Function JSONDecode(ByVal val) + Private Function aj_JSONDecode(ByVal val) val = Replace(val, "\""", """") val = Replace(val, "\\", "\") val = Replace(val, "\/", "/") @@ -208,17 +235,39 @@ Class aspJSON val = Replace(val, "\n", Chr(10)) val = Replace(val, "\r", Chr(13)) val = Replace(val, "\t", Chr(9)) - JSONDecode = Trim(val) + aj_JSONDecode = Trim(val) End Function - Private Function InlineIf(condition, returntrue, returnfalse) - If condition Then InlineIf = returntrue Else InlineIf = returnfalse + Private Function aj_InlineIf(condition, returntrue, returnfalse) + If condition Then aj_InlineIf = returntrue Else aj_InlineIf = returnfalse End Function - Private Function Strip(ByVal val, stripper) + Private Function aj_Strip(ByVal val, stripper) If Left(val, 1) = stripper Then val = Mid(val, 2) If Right(val, 1) = stripper Then val = Left(val, Len(val) - 1) - Strip = val + aj_Strip = val + End Function + + Private Function aj_MultilineTrim(TextData) + aj_MultilineTrim = aj_RegExp.Replace(TextData, "$1") + End Function + + Private Function aj_Trim(val) + aj_Trim = Trim(val) + Do While Left(aj_Trim, 1) = Chr(9) : aj_Trim = Mid(aj_Trim, 2) : Loop + Do While Right(aj_Trim, 1) = Chr(9) : aj_Trim = Left(aj_Trim, Len(aj_Trim) - 1) : Loop + aj_Trim = Trim(aj_Trim) + End Function + + Private Function aj_ReadNumericValue(ByVal val) + If Instr(val, ".") > 0 Then + numdecimals = Len(val) - Instr(val, ".") + val = Clng(Replace(val, ".", "")) + val = val / (10 ^ numdecimals) + aj_ReadNumericValue = val + Else + aj_ReadNumericValue = Clng(val) + End If End Function End Class -%> +%> \ No newline at end of file diff --git a/jwt.asp b/jwt.asp index 0ce921d..e5312bf 100644 --- a/jwt.asp +++ b/jwt.asp @@ -6,7 +6,12 @@ Function JWTEncode(dPayload, sSecret) Dim sPayload, sHeader, sBase64Payload, sBase64Header Dim sSignature, sToken - sPayload = DictionaryToJSONString(dPayload) + If Typename(dPayload) = "Dictionary" Then + sPayload = DictionaryToJSONString(dPayload) + Else + sPayload = dPayload + End If + sHeader = JWTHeaderDictionary() sBase64Payload = SafeBase64Encode(sPayload) @@ -24,7 +29,8 @@ Function SHA256SignAndEncode(sIn, sKey) Dim sSignature 'Open WSC object to access the encryption function - Set sha256 = GetObject("script:"&Server.MapPath("./external/sha256.wsc")) + Dim sha256 + Set sha256 = GetObject("script:"&Server.MapPath("/jwt/external/sha256.wsc")) 'SHA256 sign data sSignature = sha256.b64_hmac_sha256(sKey, sIn) @@ -42,4 +48,48 @@ Function JWTHeaderDictionary() JWTHeaderDictionary = DictionaryToJSONString(dOut) End Function + +' Returns decoded payload (not verify) +Function JWTDecode(token) + Dim tokenSplited, sPayload + tokenSplited = Split(token, ".") + If UBound(tokenSplited) <> 2 Then + JWTDecode = "Invalid token" + Else + sPayload = tokenSplited(1) + sPayload = SafeBase64ToBase64(sPayload) + JWTDecode = Base64Decode(sPayload) + End If +End Function + +' Updated JWTVerify function +Function JWTVerify(token, sKey) + Dim parts, header, payload, signature, signedData, computedSignature + parts = Split(token, ".") + + If UBound(parts) <> 2 Then + JWTVerify = False + Exit Function + End If + + header = parts(0) + payload = parts(1) + signature = SafeBase64ToBase64(parts(2)) + + signedData = header & "." & payload + computedSignature = SHA256SignAndEncode(signedData, sKey) + computedSignature = SafeBase64ToBase64(computedSignature) + + JWTVerify = (signature = computedSignature) +End Function + +' Helper function to convert Safe Base64 to regular Base64 +Function SafeBase64ToBase64(input) + SafeBase64ToBase64 = Replace(Replace(input, "-", "+"), "_", "/") + Dim padding + padding = Len(input) Mod 4 + If padding > 0 Then + SafeBase64ToBase64 = SafeBase64ToBase64 & String(4 - padding, "=") + End If +End Function %> diff --git a/utils.asp b/utils.asp index 16438aa..6fb9a4e 100644 --- a/utils.asp +++ b/utils.asp @@ -1,5 +1,6 @@ - - + + + <% ' The URL- and filename-safe Base64 encoding described in RFC 4648 [RFC4648], Section 5, ' with the (non URL-safe) '=' padding characters omitted, as permitted by Section 3.2. @@ -7,6 +8,7 @@ ' http://tools.ietf.org/html/rfc4648 ' http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-10 Function SafeBase64Encode(sIn) + Dim sOut sOut = Base64Encode(sIn) sOut = Base64ToSafeBase64(sOut) @@ -15,6 +17,7 @@ End Function ' Strips unsafe characters from a Base64 encoded string Function Base64ToSafeBase64(sIn) + Dim sOut sOut = Replace(sIn,"+","-") sOut = Replace(sOut,"/","_") sOut = Replace(sOut,"\r","") @@ -24,8 +27,21 @@ Function Base64ToSafeBase64(sIn) Base64ToSafeBase64 = sOut End Function +' change safe base64 to original base64 +Function SafeBase64ToBase64(sIn) + Dim removedEqualityLen + removedEqualityLen = 4 - Len(sIn) mod 4 + If removedEqualityLen = 4 Then removedEqualityLen = 0 + Dim sOut + sOut = Replace(sIn,"-","+") + sOut = Replace(sOut,"_","/") + sOut = sOut & Replace(Space(removedEqualityLen)," ","=") + SafeBase64ToBase64 = sOut +End Function + ' Converts an ASP dictionary to a JSON string Function DictionaryToJSONString(dDictionary) + Dim oJSONpayload Set oJSONpayload = New aspJSON @@ -72,9 +88,8 @@ End Function ' Returns a random string to prevent replays Function UniqueString() - Set TypeLib = CreateObject("Scriptlet.TypeLib") - UniqueString = Left(CStr(TypeLib.Guid), 38) - Set TypeLib = Nothing + Randomize + UniqueString = Year(Now) & Month(Now) & Day(Now) & Hour(Now) & Minute(Now) & Second(Now) & Int(Rnd * 1000) End Function