Skip to content

Commit

Permalink
Version 1.0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
matteobaccan committed Mar 11, 2022
1 parent 909914b commit 8410f93
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 44 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions buildandtest.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file modified lib/jwt.hrb
Binary file not shown.
Binary file modified out/jwttest.hrb
Binary file not shown.
17 changes: 16 additions & 1 deletion out/jwttest.log
Original file line number Diff line number Diff line change
Expand Up @@ -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
129 changes: 103 additions & 26 deletions src/jwt.prg
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
*
* https://datatracker.ietf.org/doc/html/rfc7519
*
* Version 1.0.1
*
*/
#include "hbclass.ch"

Expand All @@ -29,6 +27,7 @@ HIDDEN:
METHOD Base64UrlDecode( cData )
METHOD ByteToString( cData )
METHOD GetSignature( cHeader, cPayload, cSecret, cAlgorithm )
METHOD CheckPayload(aPayload, cKey)

EXPORTED:

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -85,6 +87,9 @@ EXPORTED:
// Helper method for expiration setting
METHOD GetSeconds()

// Versione
METHOD GetVersion() INLINE "1.0.1"

ENDCLASS

METHOD New() CLASS JWT
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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.
81 changes: 68 additions & 13 deletions test/jwttest.prg
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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

0 comments on commit 8410f93

Please sign in to comment.