diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..9cad1b9 --- /dev/null +++ b/build.bat @@ -0,0 +1,12 @@ +@echo off +@set path=t:\harbour\bin +@set include=t:\harbour\include + +harbour src\jwt.prg /n /w /gh +if %errorlevel% neq 0 pause + +harbour test\jwttest.prg /n /w /gh +if %errorlevel% neq 0 pause + +hbrun jwttest.hrb>jwt.log + diff --git a/src/jwt.prg b/src/jwt.prg new file mode 100644 index 0000000..e264f5e --- /dev/null +++ b/src/jwt.prg @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2019 Matteo Baccan + * https://www.baccan.it + * + * Distributed under the GPL v3 software license, see the accompanying + * file LICENSE or http://www.gnu.org/licenses/gpl.html. + * + */ +/** + * JWT Implementation + * + * https://datatracker.ietf.org/doc/html/rfc7519 + * + */ +#include "hbclass.ch" + +CLASS JWT + + DATA cSecret + DATA aHeader + DATA aPayload + DATA cError + + METHOD New() CONSTRUCTOR + + // Header + METHOD SetType( cType ) + METHOD SetContentType( cContentType ) INLINE ::aHeader[ 'cty' ] := cContentType + METHOD SetAlgorithm( cAlgorithm ) + + // Payload + METHOD SetIssuer( cIssuer ) INLINE ::aPayload[ 'iss' ] := cIssuer + METHOD SetSubject( cSubject ) INLINE ::aPayload[ 'sub' ] := cSubject + METHOD SetAudience( cAudience ) INLINE ::aPayload[ 'aud' ] := cAudience + METHOD SetExpration( nExpiration ) INLINE ::aPayload[ 'exp' ] := nExpiration + METHOD SetNotBefore( nNotBefore ) INLINE ::aPayload[ 'nbf' ] := nNotBefore + METHOD SetIssuedAt( nIssuedAt ) INLINE ::aPayload[ 'iat' ] := nIssuedAt + METHOD SetJWTId( cJWTId ) INLINE ::aPayload[ 'jti' ] := cJWTId + + // Secret + METHOD SetSecret( cSecret ) INLINE ::cSecret := cSecret + + // Cleanup data + METHOD Reset() + + // Encode a JWT + METHOD Encode() + + // Decode a JWT + METHOD Decode( cJWT, cSecret ) + + // Payload methods + METHOD SetPayloadData( cKey, uValue ) INLINE ::aPayload[ cKey ] := uValue + METHOD GetPayloadData( cKey ) INLINE ::aPayload[ cKey ] + + // Getter internal data + METHOD GetPayload() INLINE ::aPayload + METHOD GetHeader() INLINE ::aHeader + METHOD GetError() INLINE ::cError + + METHOD Base64UrlEncode( cData ) + METHOD Base64UrlDecode( cData ) + METHOD ByteToString( cData ) + METHOD GetSignature( cHeader, cPayload, cSecret, cAlgorithm ) +METHOD getposix() + +ENDCLASS + +METHOD New() CLASS JWT + ::Reset() +RETU SELF + +// Optional +METHOD SetType( cType ) CLASS JWT + LOCAL bRet := .F. + + if cType=="JWT" + ::aHeader[ 'typ' ] := cType + else + bRet := .F. + ::cError := "Invalid type [" +cType +"]" + endif + +RETU bRet + +// Mandatory +METHOD SetAlgorithm( cAlgorithm ) CLASS JWT + LOCAL bRet := .F. + + if cAlgorithm=="HS256" .OR. cAlgorithm=="HS384" .OR. cAlgorithm=="HS512" + ::aHeader[ 'alg' ] := cAlgorithm + else + bRet := .F. + ::cError := "Invalid algorithm [" +cAlgorithm +"]" + endif + +RETU bRet + +METHOD Reset() CLASS JWT + + ::aHeader := {=>} + ::aPayload := {=>} + ::cError := '' + ::cSecret := '' + +RETU NIL + + +METHOD Encode() CLASS JWT + + LOCAL cHeader + LOCAL cPayload + LOCAL cSignature + + // Encode header + cHeader := ::Base64UrlEncode( hb_jsonEncode( ::aHeader ) ) + + // Encode payload + cPayload := ::Base64UrlEncode( hb_jsonEncode( ::aPayload ) ) + + // Make signature + cSignature := ::GetSignature( cHeader, cPayload, ::cSecret, ::aHeader[ 'alg' ] ) + +// Return JWT +RETU cHeader + '.' + cPayload + '.' + cSignature + +METHOD Base64UrlEncode( cData ) CLASS JWT +RETU hb_StrReplace( hb_base64Encode( cData ), "+/=", { "-", "_", "" } ) + +METHOD Base64UrlDecode( cData ) CLASS JWT +RETU hb_base64Decode( hb_StrReplace( cData, "-_", "+/" ) ) + +METHOD ByteToString( cData ) CLASS JWT + LOCAL cRet := SPACE(LEN(cData)/2) + LOCAL nLen := LEN( cData ) + LOCAL nX, nNum + + cData := UPPER(cData) + FOR nX := 1 TO nLen STEP 2 + nNum := ( AT( SubStr( cData, nX , 1 ), "0123456789ABCDEF" ) - 1 ) * 16 + nNum += AT( SubStr( cData, nX+1, 1 ), "0123456789ABCDEF" ) - 1 + HB_BPOKE( @cRet, (nX+1)/2, nNum ) + NEXT + +RETU cRet + + METHOD GetSignature( cHeader, cPayload, cSecret, cAlgorithm ) CLASS JWT + LOCAL cSignature := "" + + DO CASE + CASE cAlgorithm=="HS256" + cSignature := ::Base64UrlEncode( ::ByteToString( HB_HMAC_SHA256( cHeader + '.' + cPayload, cSecret ) ) ) + CASE cAlgorithm=="HS384" + cSignature := ::Base64UrlEncode( ::ByteToString( HB_HMAC_SHA384( cHeader + '.' + cPayload, cSecret ) ) ) + CASE cAlgorithm=="HS512" + cSignature := ::Base64UrlEncode( ::ByteToString( HB_HMAC_SHA512( cHeader + '.' + cPayload, cSecret ) ) ) + OTHERWISE + ::cError := "INVALID ALGORITHM" + ENDCASE + RETU cSignature + +METHOD Decode( cJWT, cSecret ) CLASS JWT + + LOCAL aJWT + LOCAL cSignature, cNewSignature + + // Reset Object + ::Reset() + + // Split JWT + aJWT := HB_ATokens( cJWT, '.' ) + IF LEN(aJWT) <> 3 + ::cError := "Invalid JWT" + RETU .F. + ENDIF + + // Explode header + ::aHeader := hb_jsonDecode( ::Base64UrlDecode( aJWT[1] )) + + // Exploce payload + ::aPayload := hb_jsonDecode( ::Base64UrlDecode( aJWT[2] )) + + // Get signature + cSignature := aJWT[3] + + ::SetSecret( cSecret ) + + // Calculate new sicnature + cNewSignature := ::GetSignature( aJWT[1], aJWT[2], cSecret, ::aHeader[ 'alg' ] ) + IF ( cSignature != cNewSignature ) + ::cError := "Invalid signature" + RETU .F. + ENDIF + + // Check expiration + IF hb_HHasKey(::aPayLoad,'exp') + IF ::aPayLoad[ 'exp' ] < ::getposix() + ::cError := "Token expired" + RETU .F. + ENDIF + ENDIF + +RETU .T. + +METHOD getposix() CLASS JWT + +LOCAL posixday := date() - STOD("19700101") +LOCAL cTime := time() +LOCAL posixsec := posixday * 24 * 60 * 60 + +return posixsec + (int(val(substr(cTime,1,2))) * 3600) + (int(val(substr(cTime,4.2))) * 60) + ( int(val(substr(cTime,7,2))) ) + diff --git a/test/jwttest.prg b/test/jwttest.prg new file mode 100644 index 0000000..da492c2 --- /dev/null +++ b/test/jwttest.prg @@ -0,0 +1,65 @@ +#include "hbclass.ch" +#include "hbhrb.ch" + +function main +LOCAL handle := hb_hrbLoad( "jwt.hrb" ) +LOCAL oJWT +LOCAL cToken + +// Object +oJWT := &("JWT():new()") + +// Header +oJWT:setAlgorithm("HS256") +oJWT:setType("JWT") + +// Payload +oJWT:setSubject("1234567890") +oJWT:setPayloadData("name", "John Doe") +oJWT:setIssuedAt(1516239022) + +// Secret +oJWT:setSecret("your-256-bit-secret") + +cToken = oJWT:Encode() + +AssertEquals(cToken,"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c") + +// Test HS384 +oJWT:setAlgorithm("HS384") +oJWT:setSecret("your-384-bit-secret") +cToken = oJWT:Encode() +AssertEquals(cToken,"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.8aMsJp4VGY_Ia2s9iWrS8jARCggx0FDRn2FehblXyvGYRrVVbu3LkKKqx_MEuDjQ") + +// Test HS512 +oJWT:setAlgorithm("HS512") +oJWT:setSecret("your-512-bit-secret") +cToken = oJWT:Encode() +AssertEquals(cToken,"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ._MRZSQUbU6G_jPvXIlFsWSU-PKT203EdcU388r5EWxSxg8QpB3AmEGSo2fBfMYsOaxvzos6ehRm4CYO1MrdwUg") + +// Test none +oJWT:setAlgorithm("none") +cToken = oJWT:Encode() +AssertEquals(cToken,"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ._MRZSQUbU6G_jPvXIlFsWSU-PKT203EdcU388r5EWxSxg8QpB3AmEGSo2fBfMYsOaxvzos6ehRm4CYO1MrdwUg") + +// Token validation +AssertEquals( oJWT:Decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1hdHRlbyBCYWNjYW4iLCJpYXQiOjE1MTYyMzkwMjJ9.YR8QF52kgj0owYlP9TkEy_lNhC-Qdq38tqNNNqpvpK0", "MySecret"), .T. ) + +// Token validation +AssertEquals( oJWT:Decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1hdHRlbyBCYWNjYW4iLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0.0T90m9fq8aOuiNbycTJxCf7BiQLw9xWXxe58-zV4RpY", "MySecret"), .F. ) +? oJWT:GetError() + +hb_hrbUnload( handle ) + +RETU NIL + + +function AssertEquals( uValue, uExpected ) + IF uValue==uExpected + ? "OK - signature verified" + ELSE + ? "KO - invalid signature" + ? "Value :", uValue + ? "Expected:", uExpected + ENDIF +retu nil \ No newline at end of file