diff --git a/README.md b/README.md index eeff5a6..dbc1ced 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,11 @@ oJWT := &("JWT():new()") 3. Verify the token ``` -oJWT:Decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1hdHRlbyBCYWNjYW4iLCJpYXQiOjE1MTYyMzkwMjJ9.YR8QF52kgj0owYlP9TkEy_lNhC-Qdq38tqNNNqpvpK0", "MySecret") +oJWT:SetSecret("MySecret") +oJWT:Verify("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1hdHRlbyBCYWNjYW4iLCJpYXQiOjE1MTYyMzkwMjJ9.YR8QF52kgj0owYlP9TkEy_lNhC-Qdq38tqNNNqpvpK0") ``` -Decode return a .T. if the token is valid. Otherwise with +Verify return a .T. if the token is valid. Otherwise with ``` oJWT:GetError() diff --git a/buildandtest.bat b/buildandtest.bat index a3c4a33..a2a2208 100644 --- a/buildandtest.bat +++ b/buildandtest.bat @@ -2,10 +2,10 @@ @set path=t:\harbour\bin @set include=t:\harbour\include -harbour src\jwt.prg /n /w /gh /olib\jwt +harbour src\jwt.prg /n /w3 /es1 /gh /olib\jwt if %errorlevel% neq 0 pause -harbour test\jwttest.prg /n /w /gh /oout\jwttest +harbour test\jwttest.prg /n /w3 /gh /oout\jwttest if %errorlevel% neq 0 pause cd out diff --git a/lib/jwt.hrb b/lib/jwt.hrb index 6ef7996..b9da793 100644 Binary files a/lib/jwt.hrb and b/lib/jwt.hrb differ diff --git a/out/jwttest.hrb b/out/jwttest.hrb index d7ad2b8..dea5a0d 100644 Binary files a/out/jwttest.hrb and b/out/jwttest.hrb differ diff --git a/out/jwttest.log b/out/jwttest.log index 45bfa51..48cb0fc 100644 --- a/out/jwttest.log +++ b/out/jwttest.log @@ -5,6 +5,21 @@ OK - data verified OK - data verified OK - data verified OK - data verified -Token expired +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified +OK - data verified OK - data verified OK - data verified \ No newline at end of file diff --git a/src/jwt.prg b/src/jwt.prg index e3d63dd..7325601 100644 --- a/src/jwt.prg +++ b/src/jwt.prg @@ -11,8 +11,6 @@ * * https://datatracker.ietf.org/doc/html/rfc7519 * - * Version 1.0.1 - * */ #include "hbclass.ch" @@ -29,6 +27,7 @@ HIDDEN: METHOD Base64UrlDecode( cData ) METHOD ByteToString( cData ) METHOD GetSignature( cHeader, cPayload, cSecret, cAlgorithm ) + METHOD CheckPayload(aPayload, cKey) EXPORTED: @@ -43,24 +42,24 @@ EXPORTED: METHOD GetAlgorithm() INLINE ::aHeader[ 'alg' ] // Payload - METHOD SetIssuer( cIssuer ) INLINE ::aPayload[ 'iss' ] := cIssuer - METHOD GetIssuer() INLINE ::aPayload[ 'iss' ] - METHOD SetSubject( cSubject ) INLINE ::aPayload[ 'sub' ] := cSubject - METHOD GetSubject() INLINE ::aPayload[ 'sub' ] - METHOD SetAudience( cAudience ) INLINE ::aPayload[ 'aud' ] := cAudience - METHOD GetAudience() INLINE ::aPayload[ 'aud' ] - METHOD SetExpration( nExpiration ) INLINE ::aPayload[ 'exp' ] := nExpiration - METHOD GetExpration() INLINE ::aPayload[ 'exp' ] - METHOD SetNotBefore( nNotBefore ) INLINE ::aPayload[ 'nbf' ] := nNotBefore - METHOD GetNotBefore() INLINE ::aPayload[ 'nbf' ] - METHOD SetIssuedAt( nIssuedAt ) INLINE ::aPayload[ 'iat' ] := nIssuedAt - METHOD GetIssuedAt() INLINE ::aPayload[ 'iat' ] - METHOD SetJWTId( cJWTId ) INLINE ::aPayload[ 'jti' ] := cJWTId - METHOD GetJWTId() INLINE ::aPayload[ 'jti' ] + METHOD SetIssuer( cIssuer ) INLINE ::SetPayloadData('iss', cIssuer) + METHOD GetIssuer() INLINE ::GetPayloadData('iss') + METHOD SetSubject( cSubject ) INLINE ::SetPayloadData('sub', cSubject) + METHOD GetSubject() INLINE ::GetPayloadData('sub') + METHOD SetAudience( cAudience ) INLINE ::SetPayloadData('aud', cAudience) + METHOD GetAudience() INLINE ::GetPayloadData('aud') + METHOD SetExpration( nExpiration ) INLINE ::SetPayloadData('exp', nExpiration) + METHOD GetExpration() INLINE ::GetPayloadData('exp') + METHOD SetNotBefore( nNotBefore ) INLINE ::SetPayloadData('nbf', nNotBefore) + METHOD GetNotBefore() INLINE ::GetPayloadData('nbf') + METHOD SetIssuedAt( nIssuedAt ) INLINE ::SetPayloadData('iat', nIssuedAt) + METHOD GetIssuedAt() INLINE ::GetPayloadData('iat') + METHOD SetJWTId( cJWTId ) INLINE ::SetPayloadData('jti', cJWTId) + METHOD GetJWTId() INLINE ::GetPayloadData('jti') // Payload methods - METHOD SetPayloadData( cKey, uValue ) INLINE ::aPayload[ cKey ] := uValue - METHOD GetPayloadData( cKey ) INLINE ::aPayload[ cKey ] + METHOD SetPayloadData( cKey, uValue ) INLINE IF( uValue==NIL, hb_HDel(::aPayload,cKey), ::aPayload[cKey] := uValue) + METHOD GetPayloadData( cKey ) INLINE IF( hb_HHasKey(::aPayLoad,cKey), ::aPayload[cKey], NIL ) // Secret METHOD SetSecret( cSecret ) INLINE ::cSecret := cSecret @@ -76,7 +75,10 @@ EXPORTED: METHOD Encode() // Decode a JWT - METHOD Decode( cJWT, cSecret ) + METHOD Decode( cJWT ) + + // Decode a JWT + METHOD Verify( cJWT ) // Getter internal data with internal exposion METHOD GetPayload() INLINE hb_hClone(::aPayload) @@ -85,6 +87,9 @@ EXPORTED: // Helper method for expiration setting METHOD GetSeconds() + // Versione + METHOD GetVersion() INLINE "1.0.1" + ENDCLASS METHOD New() CLASS JWT @@ -180,10 +185,9 @@ METHOD GetSignature( cHeader, cPayload, cSecret, cAlgorithm ) CLASS JWT ENDCASE RETU cSignature -METHOD Decode( cJWT, cSecret ) CLASS JWT +METHOD Decode( cJWT ) CLASS JWT LOCAL aJWT - LOCAL cSignature, cNewSignature // Reset Object ::Reset() @@ -201,26 +205,90 @@ METHOD Decode( cJWT, cSecret ) CLASS JWT // Exploce payload ::aPayload := hb_jsonDecode( ::Base64UrlDecode( aJWT[2] )) +RETU .T. + +METHOD Verify( cJWT ) CLASS JWT + + LOCAL aJWT, aHeader, aPayload + LOCAL cSignature, cNewSignature + + // 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' ] ) + cNewSignature := ::GetSignature( aJWT[1], aJWT[2], ::cSecret, aHeader[ 'alg' ] ) IF ( cSignature != cNewSignature ) ::cError := "Invalid signature" RETU .F. ENDIF + // Check Issuer + IF !::CheckPayload(aPayload, 'iss') + ::cError := "Different issuer" + RETU .F. + ENDIF + + // Check Subject + IF !::CheckPayload(aPayload, 'sub') + ::cError := "Different subject" + RETU .F. + ENDIF + + // Check Audience + IF !::CheckPayload(aPayload, 'aud') + ::cError := "Different audience" + RETU .F. + ENDIF + // Check expiration - IF hb_HHasKey(::aPayLoad,'exp') - IF ::aPayLoad[ 'exp' ] < ::GetSeconds() + IF hb_HHasKey(aPayLoad,'exp') + IF aPayLoad[ 'exp' ] < ::GetSeconds() ::cError := "Token expired" RETU .F. ENDIF ENDIF + // Check not before + IF hb_HHasKey(aPayLoad,'nbf') + IF aPayLoad[ 'nbf' ] > ::GetSeconds() + ::cError := "Token not valid until:" +STR(aPayLoad[ 'nbf' ]) + RETU .F. + ENDIF + ENDIF + + // Check issuedAt + IF hb_HHasKey(aPayLoad,'iat') + IF aPayLoad[ 'iat' ] > ::GetSeconds() + ::cError := "Token issued in future:" +STR(aPayLoad[ 'iat' ]) + RETU .F. + ENDIF + ENDIF + + // Check JWT id + IF !::CheckPayload(aPayload, 'jti') + ::cError := "Different JWT id" + RETU .F. + ENDIF + + // Check Type + IF !::CheckPayload(aPayload, 'typ') + ::cError := "Different JWT type" + RETU .F. + ENDIF + RETU .T. METHOD GetSeconds() CLASS JWT @@ -231,3 +299,12 @@ METHOD GetSeconds() CLASS JWT RETU posixsec + (int(val(substr(cTime,1,2))) * 3600) + (int(val(substr(cTime,4.2))) * 60) + ( int(val(substr(cTime,7,2))) ) +METHOD CheckPayload(aPayload, cKey) + IF hb_HHasKey(aPayLoad,cKey) .AND. hb_HHasKey(::aPayLoad,cKey) + IF aPayLoad[ cKey ] != ::aPayLoad[ cKey ] + RETU .F. + ENDIF + ELSEIF hb_HHasKey(aPayLoad,cKey) .OR. hb_HHasKey(::aPayLoad,cKey) + RETU .F. + ENDIF +RETU .T. diff --git a/test/jwttest.prg b/test/jwttest.prg index 3e1a450..3cb3d56 100644 --- a/test/jwttest.prg +++ b/test/jwttest.prg @@ -1,7 +1,7 @@ #include "hbclass.ch" #include "hbhrb.ch" -function main +FUNCTION Main LOCAL handle := hb_hrbLoad( "../lib/jwt.hrb" ) LOCAL oJWT LOCAL cToken @@ -23,6 +23,7 @@ oJWT:setSecret("your-256-bit-secret") cToken = oJWT:Encode() +// Default token denerated by https://jwt.io/ AssertEquals(cToken,"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c") // Test HS384 @@ -35,36 +36,90 @@ AssertEquals(cToken,"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3OD oJWT:setAlgorithm("HS512") oJWT:setSecret("your-512-bit-secret") cToken = oJWT:Encode() -AssertEquals(cToken,"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ._MRZSQUbU6G_jPvXIlFsWSU-PKT203EdcU388r5EWxSxg8QpB3AmEGSo2fBfMYsOaxvzos6ehRm4CYO1MrdwUg") +AssertEquals(cToken,"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ._MRZSQUbU6G_jPvXIlFsWSU-PKT203EdcU388r5EWxSxg8QpB3AmEGSo2fBfMYsOaxvzos6ehRm4CYO1MrdwUg", oJWT:getError() ) -// Test none -oJWT:setAlgorithm("none") +// Token validation +AssertEquals( oJWT:Verify("eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ._MRZSQUbU6G_jPvXIlFsWSU-PKT203EdcU388r5EWxSxg8QpB3AmEGSo2fBfMYsOaxvzos6ehRm4CYO1MrdwUg"), .T., oJWT:getError() ) + +oJWT:SetIssuer('Matteo') cToken = oJWT:Encode() -AssertEquals(cToken,"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ._MRZSQUbU6G_jPvXIlFsWSU-PKT203EdcU388r5EWxSxg8QpB3AmEGSo2fBfMYsOaxvzos6ehRm4CYO1MrdwUg") +AssertEquals( oJWT:Verify(cToken), .T., oJWT:getError() ) +AssertEquals( oJWT:Decode(cToken), .T., oJWT:getError() ) -// Token validation -AssertEquals( oJWT:Decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1hdHRlbyBCYWNjYW4iLCJpYXQiOjE1MTYyMzkwMjJ9.YR8QF52kgj0owYlP9TkEy_lNhC-Qdq38tqNNNqpvpK0", "MySecret"), .T. ) +// Verify is false because secret is reset by Decode +AssertEquals( oJWT:Verify(cToken), .F., oJWT:getError() ) -// Token validation -AssertEquals( oJWT:Decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1hdHRlbyBCYWNjYW4iLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0.0T90m9fq8aOuiNbycTJxCf7BiQLw9xWXxe58-zV4RpY", "MySecret"), .F. ) -? oJWT:GetError() +// Recover secret +oJWT:setSecret("your-512-bit-secret") +AssertEquals( oJWT:Verify(cToken), .T., oJWT:getError() ) + +// test different odience +oJWT:SetSubject("new subject") +AssertEquals( oJWT:Verify(cToken), .F., oJWT:getError() ) +oJWT:SetSubject("1234567890") +AssertEquals( oJWT:Verify(cToken), .T., oJWT:getError() ) + +// test different odience +oJWT:SetAudience("new odience") +AssertEquals( oJWT:Verify(cToken), .F., oJWT:getError() ) +oJWT:SetAudience(NIL) +AssertEquals( oJWT:Verify(cToken), .T., oJWT:getError() ) + +// Expired token +oJWT:SetExpration( oJWT:GetSeconds()-1 ) +cToken = oJWT:Encode() +AssertEquals( oJWT:Verify(cToken), .F., oJWT:getError() ) +oJWT:SetExpration( oJWT:GetSeconds()+1 ) +cToken = oJWT:Encode() +AssertEquals( oJWT:Verify(cToken), .T., oJWT:getError() ) + +// NotBefore +oJWT:SetNotBefore( oJWT:GetSeconds()+2 ) +cToken = oJWT:Encode() +AssertEquals( oJWT:Verify(cToken), .F., oJWT:getError() ) +oJWT:SetNotBefore( oJWT:GetSeconds() ) +cToken = oJWT:Encode() +AssertEquals( oJWT:Verify(cToken), .T., oJWT:getError() ) + +// Issued at +oJWT:SetIssuedAt( oJWT:GetSeconds()+1 ) +cToken = oJWT:Encode() +AssertEquals( oJWT:Verify(cToken), .F., oJWT:getError() ) +oJWT:SetIssuedAt( oJWT:GetSeconds() ) +cToken = oJWT:Encode() +AssertEquals( oJWT:Verify(cToken), .T., oJWT:getError() ) + +// JWTId +oJWT:SetJWTId("ID:100") +AssertEquals( oJWT:Verify(cToken), .F., oJWT:getError() ) +oJWT:SetJWTId(NIL) +AssertEquals( oJWT:Verify(cToken), .T., oJWT:getError() ) + +// Token decode +AssertEquals( oJWT:Decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1hdHRlbyBCYWNjYW4iLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0.0T90m9fq8aOuiNbycTJxCf7BiQLw9xWXxe58-zV4RpY"), .T., oJWT:getError() ) // Check internal data exposion -AssertEquals(oJWT:GetHeader()['alg'], oJWT:GetAlgorithm()) +AssertEquals(oJWT:GetHeader()['alg'], oJWT:GetAlgorithm(), oJWT:getError() ) oJWT:GetHeader()['alg'] := 'dddd' -AssertEquals(oJWT:GetHeader()['alg'], oJWT:GetAlgorithm()) +AssertEquals(oJWT:GetHeader()['alg'], oJWT:GetAlgorithm(), oJWT:getError() ) + +// Versione +AssertEquals(oJWT:GetVersion(), "1.0.1" ) hb_hrbUnload( handle ) RETU NIL -function AssertEquals( uValue, uExpected ) +function AssertEquals( uValue, uExpected, cMessage ) IF uValue==uExpected ? "OK - data verified" ELSE ? "KO - invalid data" ? "Value :", uValue ? "Expected:", uExpected + IF cMessage!=NIL .AND. !EMPTY(cMessage) + ? cMessage + ENDIF ENDIF retu nil \ No newline at end of file