Skip to content

Commit

Permalink
First version: support for HS256, HS384, HS512, decode and expiration
Browse files Browse the repository at this point in the history
  • Loading branch information
matteobaccan committed Mar 10, 2022
1 parent bcdb512 commit b658822
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 0 deletions.
12 changes: 12 additions & 0 deletions build.bat
Original file line number Diff line number Diff line change
@@ -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

212 changes: 212 additions & 0 deletions src/jwt.prg
Original file line number Diff line number Diff line change
@@ -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))) )

65 changes: 65 additions & 0 deletions test/jwttest.prg
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit b658822

Please sign in to comment.