├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── jwt.nim ├── jwt.nimble ├── jwt └── private │ ├── claims.nim │ ├── crypto.nim │ ├── jose.nim │ └── utils.nim └── tests ├── .gitignore ├── nim.cfg ├── t_claims.nim └── t_jwt.nim /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | Test: 10 | if: | 11 | !contains(github.event.head_commit.message, '[skip ci]') 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest] 16 | nim-channel: [stable, devel] 17 | 18 | name: ${{ matrix.os }}-${{ matrix.nim-channel }} 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Setup nim 24 | uses: jiro4989/setup-nim-action@v1 25 | with: 26 | nim-version: ${{ matrix.nim-channel }} 27 | 28 | - name: Test 29 | shell: bash 30 | run: | 31 | nim --version 32 | nimble install -dy 33 | nimble test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !**/ 3 | !*.* 4 | !LICENSE 5 | nimcache/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Yuriy Glukhov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JWT Implementation for Nim [![Build Status](https://github.com/yglukhov/nim-jwt/workflows/CI/badge.svg?branch=master)](https://github.com/yglukhov/nim-jwt/actions?query=branch%3Amaster) 2 | =============================== 3 | 4 | This is a implementation of JSON Web Tokens for Nim, it allows for the following operations to be performed: 5 | 6 | `proc toJWT*(node: JsonNode): JWT` - parse a JSON object representing a JWT token to create a JWT token object. 7 | 8 | `proc toJWT*(s: string): JWT` - parse a base64 string to decode it to a JWT token object 9 | 10 | `sign*(token: var JWT, secret: string)` - sign a token. Creates a `signature` property on the given token and assigns the signature to it. 11 | 12 | `proc verify*(token: JWT, secret: string, alg: SignatureAlgorithm): bool` - verify a token (typically on your incoming requests) 13 | 14 | `proc $*(token: JWT): string` - creates a b64url string from the token 15 | 16 | ## Installation 17 | After installing nim's package manager `nimble` execute this: 18 | `nimble install jwt` 19 | 20 | ## Example 21 | 22 | An example to demonstrate use with a userId 23 | 24 | ```nim 25 | import jwt, times, json, tables 26 | 27 | var secret = "secret" 28 | 29 | proc sign(userId: string): string = 30 | var token = toJWT(%*{ 31 | "header": { 32 | "alg": "HS256", 33 | "typ": "JWT" 34 | }, 35 | "claims": { 36 | "userId": userId, 37 | "exp": (getTime() + 1.days).toUnix() 38 | } 39 | }) 40 | 41 | token.sign(secret) 42 | 43 | result = $token 44 | 45 | proc verify(token: string): bool = 46 | try: 47 | let jwtToken = token.toJWT() 48 | result = jwtToken.verify(secret, HS256) 49 | except InvalidToken: 50 | result = false 51 | 52 | proc decode(token: string): string = 53 | let jwt = token.toJWT() 54 | result = $jwt.claims["userId"].node.str 55 | 56 | ``` 57 | 58 | Getting google api oauth2 token: 59 | ```nim 60 | import jwt, json, times, httpclient, cgi 61 | 62 | const email = "username@api-12345-12345.iam.gserviceaccount.com" # Acquired from google api console 63 | const scope = "https://www.googleapis.com/auth/androidpublisher" # Define needed scope 64 | const privateKey = """ 65 | -----BEGIN PRIVATE KEY----- 66 | The key should be Acquired from google api console 67 | -----END PRIVATE KEY----- 68 | """ 69 | 70 | var tok = initJWT( 71 | header = JOSEHeader(alg: RS256, typ: "JWT"), 72 | claims = toClaims(%*{ 73 | "iss": email, 74 | "scope": scope, 75 | "aud": "https://www.googleapis.com/oauth2/v4/token", 76 | "exp": int(epochTime() + 60 * 60), 77 | "iat": int(epochTime()) 78 | })) 79 | 80 | tok.sign(privateKey) 81 | 82 | let postdata = "grant_type=" & encodeUrl("urn:ietf:params:oauth:grant-type:jwt-bearer") & "&assertion=" & $tok 83 | 84 | proc request(url: string, body: string): string = 85 | var client = newHttpClient() 86 | client.headers = newHttpHeaders({ "Content-Length": $body.len, "Content-Type": "application/x-www-form-urlencoded" }) 87 | result = client.postContent(url, body) 88 | client.close() 89 | 90 | let resp = request("https://www.googleapis.com/oauth2/v4/token", postdata).parseJson() 91 | echo "Access token is: ", resp["access_token"].str 92 | ``` 93 | -------------------------------------------------------------------------------- /jwt.nim: -------------------------------------------------------------------------------- 1 | import json, strutils, tables, times 2 | import bearssl 3 | 4 | from jwt/private/crypto import nil 5 | 6 | import jwt/private/[claims, jose, utils] 7 | 8 | type 9 | InvalidToken* = object of ValueError 10 | 11 | JWT* = object 12 | headerB64: string 13 | claimsB64: string 14 | header*: JsonNode 15 | claims*: TableRef[string, Claim] 16 | signature*: seq[byte] 17 | 18 | export claims 19 | export jose 20 | 21 | proc splitToken(s: string): seq[string] = 22 | let parts = s.split(".") 23 | if parts.len != 3: 24 | raise newException(InvalidToken, "Invalid token") 25 | result = parts 26 | 27 | proc initJWT*(header: JsonNode, claims: TableRef[string, Claim], signature: seq[byte] = @[]): JWT = 28 | JWT( 29 | headerB64: header.toBase64, 30 | claimsB64: claims.toBase64, 31 | header: header, 32 | claims: claims, 33 | signature: signature 34 | ) 35 | 36 | # Load up a b64url string to JWT 37 | proc toJWT*(s: string): JWT = 38 | var parts = splitToken(s) 39 | let 40 | headerB64 = parts[0] 41 | claimsB64 = parts[1] 42 | headerJson = parseJson(decodeUrlSafeAsString(headerB64)) 43 | claimsJson = parseJson(decodeUrlSafeAsString(claimsB64)) 44 | signature = decodeUrlSafe(parts[2]) 45 | 46 | JWT( 47 | headerB64: headerB64, 48 | claimsB64: claimsB64, 49 | header: headerJson.toHeader(), 50 | claims: claimsJson.toClaims(), 51 | signature: signature 52 | ) 53 | 54 | proc toJWT*(node: JsonNode): JWT = 55 | initJWT(node["header"].toHeader, node["claims"].toClaims) 56 | 57 | # Encodes the raw signature to b64url 58 | proc signatureToB64(token: JWT): string = 59 | assert token.signature.len != 0 60 | result = encodeUrlSafe(token.signature) 61 | 62 | proc loaded*(token: JWT): string = 63 | token.headerB64 & "." & token.claimsB64 64 | 65 | proc parsed*(token: JWT): string = 66 | result = token.header.toBase64 & "." & token.claims.toBase64 67 | 68 | # Signs a string with a secret 69 | proc signString*(toSign: string, secret: string, algorithm: SignatureAlgorithm = HS256): seq[byte] = 70 | template hsSign(meth: typed): seq[byte] = 71 | crypto.bearHMAC(addr meth, secret, toSign) 72 | 73 | template rsSign(hc, oid: typed, hashLen: int): seq[byte] = 74 | crypto.bearSignRSPem(toSign, secret, addr hc, oid, hashLen) 75 | 76 | template ecSign(hc: typed): seq[byte] = 77 | crypto.bearSignECPem(toSign, secret, addr hc) 78 | 79 | case algorithm 80 | of HS256: 81 | return hsSign(sha256Vtable) 82 | of HS384: 83 | return hsSign(sha384Vtable) 84 | of HS512: 85 | return hsSign(sha512Vtable) 86 | of RS256: 87 | return rsSign(sha256Vtable, HASH_OID_SHA256, sha256SIZE) 88 | of RS384: 89 | return rsSign(sha384Vtable, HASH_OID_SHA384, sha384SIZE) 90 | of RS512: 91 | return rsSign(sha512Vtable, HASH_OID_SHA512, sha512SIZE) 92 | of ES256: 93 | return ecSign(sha256Vtable) 94 | of ES384: 95 | return ecSign(sha384Vtable) 96 | of ES512: 97 | return ecSign(sha512Vtable) 98 | 99 | # of ES384: 100 | # return rsSign(crypto.EVP_sha384()) 101 | else: 102 | raise newException(UnsupportedAlgorithm, $algorithm & " isn't supported") 103 | 104 | # Verify that the token is not tampered with 105 | proc verifySignature*(data: string, signature: seq[byte], secret: string, 106 | alg: SignatureAlgorithm): bool = 107 | case alg 108 | of HS256, HS384, HS512: 109 | let dataSignature = signString(data, secret, alg) 110 | result = dataSignature == signature 111 | of RS256: 112 | result = crypto.bearVerifyRSPem(data, secret, signature, addr sha256Vtable, HASH_OID_SHA256, sha256SIZE) 113 | of RS384: 114 | result = crypto.bearVerifyRSPem(data, secret, signature, addr sha384Vtable, HASH_OID_SHA384, sha384SIZE) 115 | of RS512: 116 | result = crypto.bearVerifyRSPem(data, secret, signature, addr sha512Vtable, HASH_OID_SHA512, sha512SIZE) 117 | of ES256: 118 | result = crypto.bearVerifyECPem(data, secret, signature, addr sha256Vtable, sha256SIZE) 119 | of ES384: 120 | result = crypto.bearVerifyECPem(data, secret, signature, addr sha384Vtable, sha384SIZE) 121 | of ES512: 122 | result = crypto.bearVerifyECPem(data, secret, signature, addr sha512Vtable, sha512SIZE) 123 | 124 | else: 125 | assert(false, "Not implemented") 126 | 127 | proc sign*(token: var JWT, secret: string) = 128 | assert token.signature.len == 0 129 | token.signature = signString(token.parsed, secret, token.header.alg) 130 | 131 | # Verify a token typically an incoming request 132 | proc verify*(token: JWT, secret: string, alg: SignatureAlgorithm): bool = 133 | token.header.alg == alg and verifySignature(token.loaded, token.signature, secret, alg) 134 | 135 | proc toString*(token: JWT): string = 136 | token.header.toBase64 & "." & token.claims.toBase64 & "." & token.signatureToB64 137 | 138 | 139 | proc `$`*(token: JWT): string = 140 | token.toString 141 | 142 | 143 | proc `%`*(token: JWT): JsonNode = 144 | let s = $token 145 | %s 146 | 147 | proc verifyTimeClaims*(token: JWT) = 148 | let now = getTime() 149 | if token.claims.hasKey("nbf"): 150 | let nbf = token.claims["nbf"].getClaimTime 151 | if now < nbf: 152 | raise newException(InvalidToken, "Token cant be used yet") 153 | 154 | if token.claims.hasKey("exp"): 155 | let exp = token.claims["exp"].getClaimTime 156 | if now > exp : 157 | raise newException(InvalidToken, "Token is expired") 158 | 159 | # Verify token nbf exp 160 | -------------------------------------------------------------------------------- /jwt.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = "0.2" 3 | author = "Yuriy Glukhov" 4 | description = "JSON Web Tokens for Nim" 5 | license = "MIT" 6 | 7 | # Deps 8 | requires "nim >= 0.19.0" 9 | requires "bearssl" 10 | requires "https://github.com/yglukhov/bearssl_pkey_decoder" 11 | -------------------------------------------------------------------------------- /jwt/private/claims.nim: -------------------------------------------------------------------------------- 1 | import json, strutils, times, tables 2 | 3 | import utils 4 | 5 | 6 | type 7 | ClaimKind* = enum 8 | ISS, 9 | SUB, 10 | NBF, 11 | EXP, 12 | AUD, 13 | IAT, 14 | JTI, 15 | GENERAL 16 | 17 | Claim* = ref ClaimObj 18 | ClaimObj* {.acyclic.} = object 19 | node*: JsonNode 20 | kind*: ClaimKind 21 | 22 | 23 | 24 | proc newClaims*(claims: varargs[tuple[key: string, val: Claim]]): TableRef[string, Claim] = 25 | result = newTable[string, Claim](claims) 26 | 27 | 28 | proc newClaim*(k: ClaimKind, node: JsonNode): Claim = 29 | new result 30 | result.kind = k 31 | result.node = node 32 | 33 | # ISS 34 | proc newISS*(node: JsonNode): Claim = 35 | checkJsonNodeKind(node, JString) 36 | return newClaim(ISS, node) 37 | 38 | # SUB 39 | proc newSUB*(node: JsonNode): Claim = 40 | checkJsonNodeKind(node, JString) 41 | return newClaim(SUB, node) 42 | 43 | # AUD 44 | proc newAUD*(node: JsonNode): Claim = 45 | if node.kind != JArray and node.kind != JString: 46 | raise newException(ValueError, "Invalid kind") 47 | return newClaim(AUD, node) 48 | 49 | proc newAUD*(recipients: seq[string]): Claim = 50 | var node = newJArray() 51 | for r in recipients: 52 | node.add(%r) 53 | result = newAUD(node) 54 | 55 | proc newAUD*(recipient: string): Claim = return newAUD(@[recipient]) 56 | 57 | proc newAUD*(recipients: varargs[string]): Claim = return newAUD(@recipients) 58 | 59 | 60 | # Claims that have any kind of time 61 | proc newTimeClaim*(k: ClaimKind, j: JsonNode): Claim = 62 | # Check that the json kind is int.. 63 | checkJsonNodeKind(j, JInt) 64 | return newClaim(k, j) 65 | 66 | proc newTimeClaim*(k: ClaimKind, s: string): Claim = 67 | return newTimeClaim(k, %parseInt(s)) 68 | 69 | proc newTimeClaim*(k: ClaimKind, i: int64): Claim = 70 | return newTimeClaim(k, %i) 71 | 72 | # Returns the claimKeyms value as a time 73 | proc getClaimTime*(c: Claim): Time = 74 | result = fromUnix(c.node.num) 75 | 76 | # NBF 77 | proc newNBF*(s: string): Claim = return newTimeClaim(NBF, s) 78 | 79 | proc newNBF*(j: JsonNode): Claim = return newTimeClaim(NBF, j) 80 | 81 | proc newNBF*(i: int64): Claim = return newTimeClaim(NBF, i) 82 | 83 | # EXP 84 | proc newEXP*(s: string): Claim = return newTimeClaim(EXP, s) 85 | 86 | proc newEXP*(j: JsonNode): Claim = return newTimeClaim(EXP, j) 87 | 88 | proc newEXP*(i: int64): Claim = return newTimeClaim(EXP, i) 89 | 90 | # IAT 91 | proc newIAT*(s: string): Claim = return newTimeClaim(IAT, s) 92 | 93 | proc newIAT*(j: JsonNode): Claim = return newTimeClaim(IAT, j) 94 | 95 | proc newIAT*(i: int64): Claim = return newTimeClaim(IAT, i) 96 | 97 | # JTI 98 | proc newJTI*(j: JsonNode): Claim = 99 | assert j.kind == JString 100 | return newClaim(JTI, j) 101 | 102 | proc newJTI*(s: string): Claim = 103 | return newJTI(%s) 104 | 105 | 106 | proc toClaims*(j: JsonNode): TableRef[string, Claim] = 107 | result = newClaims() 108 | 109 | for claimKey, claimNode in j: 110 | case claimKey: 111 | of "iss": 112 | result[claimKey] = newISS(claimNode) 113 | of "sub": 114 | result[claimKey] = newSUB(claimNode) 115 | of "aud": 116 | result[claimKey] = newAUD(claimNode) 117 | of "nbf": 118 | result[claimKey] = newNBF(claimNode) 119 | of "exp": 120 | result[claimKey] = newEXP(claimNode) 121 | of "iat": 122 | result[claimKey] = newIAT(claimNode) 123 | of "jti": 124 | result[claimKey] = newJTI(claimNode) 125 | else: 126 | result[claimKey] = newClaim(GENERAL, claimNode) 127 | 128 | 129 | proc `%`*(c: Claim): JsonNode = 130 | result = c.node 131 | 132 | 133 | proc `%`*(claims: TableRef[string, Claim]): JsonNode = 134 | result = newJObject() 135 | for k, v in claims: 136 | result[k] = %v 137 | 138 | 139 | proc toBase64*(claims: TableRef[string, Claim]): string = 140 | let asJson = %claims 141 | result = encodeUrlSafe($asJson) 142 | -------------------------------------------------------------------------------- /jwt/private/crypto.nim: -------------------------------------------------------------------------------- 1 | import bearssl, bearssl_pkey_decoder 2 | 3 | # This pragma should be the same as in nim-bearssl/decls.nim 4 | {.pragma: bearSslFunc, cdecl, gcsafe, noSideEffect, raises: [].} 5 | 6 | proc bearHMAC*(digestVtable: ptr HashClass; key, d: string): seq[byte] = 7 | var hKey: HmacKeyContext 8 | var hCtx: HmacContext 9 | hmacKeyInit(hKey, digestVtable, key.cstring, key.len.uint) 10 | hmacInit(hCtx, hKey, 0) 11 | hmacUpdate(hCtx, d.cstring, d.len.uint) 12 | let sz = hmacSize(hCtx) 13 | result = newSeqUninitialized[byte](sz) 14 | discard hmacOut(hCtx, addr result[0]) 15 | 16 | proc invalidPemKey() = 17 | raise newException(ValueError, "Invalid PEM encoding") 18 | 19 | proc pemDecoderLoop(pem: string, prc: proc(ctx: pointer, pbytes: pointer, nbytes: uint) {.bearSslFunc.}, ctx: pointer) = 20 | var pemCtx: PemDecoderContext 21 | pemDecoderInit(pemCtx) 22 | var length = len(pem) 23 | var offset = 0 24 | var inobj = false 25 | while length > 0: 26 | var tlen = pemDecoderPush(pemCtx, 27 | unsafeAddr pem[offset], length.uint).int 28 | offset = offset + tlen 29 | length = length - tlen 30 | 31 | let event = pemDecoderEvent(pemCtx) 32 | if event == PEM_BEGIN_OBJ: 33 | inobj = true 34 | pemDecoderSetdest(pemCtx, prc, ctx) 35 | elif event == PEM_END_OBJ: 36 | if inobj: 37 | inobj = false 38 | else: 39 | break 40 | elif event == 0 and length == 0: 41 | break 42 | else: 43 | invalidPemKey() 44 | 45 | proc decodeFromPem(skCtx: var SkeyDecoderContext, pem: string) = 46 | skeyDecoderInit(skCtx) 47 | pemDecoderLoop(pem, cast[proc(ctx: pointer, pbytes: pointer, nbytes: uint) {.bearSslFunc.}](skeyDecoderPush), addr skCtx) 48 | if skeyDecoderLastError(skCtx) != 0: invalidPemKey() 49 | 50 | proc decodeFromPem(pkCtx: var PkeyDecoderContext, pem: string) = 51 | pkeyDecoderInit(addr pkCtx) 52 | pemDecoderLoop(pem, cast[proc(ctx: pointer, pbytes: pointer, nbytes: uint) {.bearSslFunc.}](pkeyDecoderPush), addr pkCtx) 53 | if pkeyDecoderLastError(addr pkCtx) != 0: invalidPemKey() 54 | 55 | proc calcHash(alg: ptr HashClass, data: string, output: var array[64, byte]) = 56 | var ctx: array[512, byte] 57 | let pCtx = cast[ptr ptr HashClass](addr ctx[0]) 58 | assert(alg.contextSize <= sizeof(ctx).uint) 59 | alg.init(pCtx) 60 | if data.len > 0: 61 | alg.update(pCtx, unsafeAddr data[0], data.len.uint) 62 | alg.`out`(pCtx, addr output[0]) 63 | 64 | proc bearSignRSPem*(data, key: string, alg: ptr HashClass, hashOid: cstring, hashLen: int): seq[byte] = 65 | # Step 1. Extract RSA key from `key` in PEM format 66 | var skCtx: SkeyDecoderContext 67 | decodeFromPem(skCtx, key) 68 | if skeyDecoderKeyType(skCtx) != KEYTYPE_RSA: 69 | invalidPemKey() 70 | 71 | template pk(): RsaPrivateKey = skCtx.key.rsa 72 | 73 | # Step 2. Hash! 74 | var digest: array[64, byte] 75 | calcHash(alg, data, digest) 76 | 77 | let sigLen = (pk.nBitlen + 7) div 8 78 | result = newSeqUninitialized[byte](sigLen) 79 | let s = rsaPkcs1SignGetDefault() 80 | assert(not s.isNil) 81 | if s(cast[ptr byte](hashOid), addr digest[0], hashLen.uint, addr pk, addr result[0]) != 1: 82 | raise newException(ValueError, "Could not sign") 83 | 84 | proc bearVerifyRSPem*(data, key: string, sig: openarray[byte], alg: ptr HashClass, hashOid: cstring, hashLen: int): bool = 85 | # Step 1. Extract RSA key from `key` in PEM format 86 | var pkCtx: PkeyDecoderContext 87 | decodeFromPem(pkCtx, key) 88 | if pkeyDecoderKeyType(addr pkCtx) != KEYTYPE_RSA: 89 | invalidPemKey() 90 | template pk(): RsaPublicKey = pkCtx.key.rsa 91 | 92 | var digest: array[64, byte] 93 | calcHash(alg, data, digest) 94 | 95 | let s = rsaPkcs1VrfyGetDefault() 96 | var digest2: array[64, byte] 97 | 98 | if s(unsafeAddr sig[0], sig.len.uint, cast[ptr byte](hashOid), hashLen.uint, addr pk, addr digest2[0]) != 1: 99 | return false 100 | 101 | digest == digest2 102 | 103 | proc bearSignECPem*(data, key: string, alg: ptr HashClass): seq[byte] = 104 | # Step 1. Extract EC Priv key from `key` in PEM format 105 | var skCtx: SkeyDecoderContext 106 | decodeFromPem(skCtx, key) 107 | if skeyDecoderKeyType(skCtx) != KEYTYPE_EC: 108 | invalidPemKey() 109 | 110 | template pk(): EcPrivateKey = skCtx.key.ec 111 | 112 | # Step 2. Hash! 113 | var digest: array[64, byte] 114 | calcHash(alg, data, digest) 115 | 116 | const maxSigLen = 140 # according to bearssl doc 117 | 118 | result = newSeqUninitialized[byte](maxSigLen) 119 | let s = ecdsaSignRawGetDefault() 120 | assert(not s.isNil) 121 | let impl = ecGetDefault() 122 | let sz = s(impl, alg, addr digest[0], addr pk, cast[ptr char](addr result[0])) 123 | assert(sz <= maxSigLen) 124 | result.setLen(sz) 125 | 126 | proc bearVerifyECPem*(data, key: string, sig: openarray[byte], alg: ptr HashClass, hashLen: int): bool = 127 | # Step 1. Extract EC Pub key from `key` in PEM format 128 | var pkCtx: PkeyDecoderContext 129 | decodeFromPem(pkCtx, key) 130 | if pkeyDecoderKeyType(addr pkCtx) != KEYTYPE_EC: 131 | invalidPemKey() 132 | template pk(): EcPublicKey = pkCtx.key.ec 133 | 134 | # bearssl ecdsaVrfy requires pubkey to be prepended with 0x04 byte, do it here 135 | assert((pk.q == addr pkCtx.key_data) and pk.qlen < sizeof(pkCtx.key_data).uint) 136 | moveMem(addr pkCtx.key_data[1], addr pkCtx.key_data[0], pk.qlen) 137 | pkCtx.key_data[0] = 0x04 138 | inc pk.qlen 139 | 140 | var digest: array[64, byte] 141 | calcHash(alg, data, digest) 142 | 143 | let impl = ecGetDefault() 144 | let s = ecdsaVrfyRawGetDefault() 145 | result = s(impl, addr digest[0], hashLen.uint, addr pk, unsafeAddr sig[0], sig.len.uint) == 1 146 | -------------------------------------------------------------------------------- /jwt/private/jose.nim: -------------------------------------------------------------------------------- 1 | import json, strutils 2 | 3 | import utils 4 | 5 | type 6 | UnsupportedAlgorithm* = object of ValueError 7 | 8 | SignatureAlgorithm* = enum 9 | NONE 10 | HS256 11 | HS384 12 | HS512 13 | RS256 14 | RS384 15 | RS512 16 | ES256 17 | ES384 18 | ES512 19 | 20 | proc strToSignatureAlgorithm(s: string): SignatureAlgorithm = 21 | try: 22 | result = parseEnum[SignatureAlgorithm](s) 23 | except ValueError: 24 | raise newException(UnsupportedAlgorithm, "$# isn't supported" % s) 25 | 26 | 27 | proc toHeader*(j: JsonNode): JsonNode = 28 | # Check that the keys are present so we dont blow up. 29 | result = newJObject() 30 | utils.checkKeysExists(j, "alg", "typ") 31 | # we do this attribute by attribute because some tests depend on the order of these keys 32 | result["alg"] = %strToSignatureAlgorithm(j["alg"].getStr()) 33 | result["typ"] = j["typ"] 34 | for key in j.keys: 35 | if not result.hasKey(key): 36 | result[key] = j[key] 37 | 38 | proc alg*(j: JsonNode): SignatureAlgorithm = 39 | doAssert j.hasKey("alg") 40 | return j["alg"].getStr().strToSignatureAlgorithm() 41 | 42 | proc `%`*(alg: SignatureAlgorithm): JsonNode = 43 | let s = $alg 44 | return %s 45 | 46 | 47 | proc toBase64*(h: JsonNode): string = 48 | result = encodeUrlSafe($h) 49 | -------------------------------------------------------------------------------- /jwt/private/utils.nim: -------------------------------------------------------------------------------- 1 | import json, strutils 2 | 3 | from base64 import nil 4 | 5 | 6 | proc checkJsonNodeKind*(node: JsonNode, kind: JsonNodeKind) = 7 | # Check that a given JsonNode has a given kind, raise ValueError if not 8 | if node.kind != kind: 9 | raise newException(ValueError, "Invalid kind") 10 | 11 | 12 | proc checkKeysExists*(node: JsonNode, keys: varargs[string]) = 13 | for key in keys: 14 | if not node.hasKey(key): 15 | raise newException(KeyError, "$# is not present." % key) 16 | 17 | proc encodeUrlSafe*(s: openarray[byte]): string = 18 | when (NimMajor >= 1 and (NimMinor >= 1 or NimPatch >= 2)) or NimMajor >= 2: 19 | result = base64.encode(s) 20 | else: 21 | result = base64.encode(s, newLine="") 22 | while result.endsWith("="): 23 | result.setLen(result.len - 1) 24 | result = result.replace('+', '-').replace('/', '_') 25 | 26 | proc encodeUrlSafe*(s: openarray[char]): string {.inline.} = 27 | encodeUrlSafe(s.toOpenArrayByte(s.low, s.high)) 28 | 29 | proc decodeUrlSafeAsString*(s: string): string = 30 | var s = s.replace('-', '+').replace('_', '/') 31 | while s.len mod 4 > 0: 32 | s &= "=" 33 | base64.decode(s) 34 | 35 | proc decodeUrlSafe*(s: string): seq[byte] = 36 | cast[seq[byte]](decodeUrlSafeAsString(s)) 37 | 38 | proc toUtf*(s: seq[byte]): string = 39 | result = newString(s.len) 40 | if s.len > 0: 41 | copyMem(addr result[0], unsafeAddr s[0], s.len) 42 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.dSYM 2 | -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | path = "$projectPath/../src" 2 | hints = off 3 | linedir = on 4 | debuginfo 5 | stacktrace = on 6 | linetrace = on -------------------------------------------------------------------------------- /tests/t_claims.nim: -------------------------------------------------------------------------------- 1 | import json, unittest 2 | 3 | import ../jwt 4 | 5 | suite "Claim ops": 6 | test "Create claims from JSON": 7 | let asJson = %{ 8 | "iss": %"jane", 9 | "sub": %"john", 10 | "nbf": %1234, 11 | "iat": %1234, 12 | "exp": %1234, 13 | "jti": %"token-id", 14 | "foo": %{"bar": %1} 15 | } 16 | let claims = asJson.toClaims 17 | let toJson = %claims 18 | 19 | assert asJson.len == toJson.len 20 | for k, v in asJson: 21 | assert v == toJson[k] 22 | -------------------------------------------------------------------------------- /tests/t_jwt.nim: -------------------------------------------------------------------------------- 1 | import json, times, unittest 2 | 3 | import ../jwt 4 | 5 | proc getToken(claims: JsonNode = newJObject(), header: JsonNode = newJObject()): JWT = 6 | for k, v in %*{"alg": "HS512", "typ": "JWT"}: 7 | if not header.hasKey(k): 8 | header[k] = v 9 | 10 | initJWT(header.toHeader, claims.toClaims) 11 | 12 | proc tokenWithAlg(alg: string): JWT = 13 | let header = %*{ "typ": "JWT", "alg": alg } 14 | let claims = %*{ "sub": "1234567890", 15 | "name": "John Doe", 16 | "iat": 1516239022 } 17 | initJWT(header.toHeader, claims.toClaims) 18 | 19 | proc signedHSToken(alg: string): JWT = 20 | result = tokenWithAlg(alg) 21 | result.sign("your-256-secret") 22 | 23 | const 24 | rsPrivateKey = """-----BEGIN RSA PRIVATE KEY----- 25 | MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw 26 | kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr 27 | m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi 28 | NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV 29 | 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 30 | QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs 31 | kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go 32 | amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM 33 | +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 34 | D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC 35 | 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y 36 | lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ 37 | hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp 38 | bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X 39 | +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B 40 | BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC 41 | 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx 42 | QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz 43 | 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 44 | Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 45 | NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j 46 | 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma 47 | 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K 48 | y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB 49 | jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= 50 | -----END RSA PRIVATE KEY-----""" 51 | rsPublicKey = """-----BEGIN PUBLIC KEY----- 52 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv 53 | vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc 54 | aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy 55 | tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 56 | e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb 57 | V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 58 | MwIDAQAB 59 | -----END PUBLIC KEY-----""" 60 | ec256PrivKey = """-----BEGIN PRIVATE KEY----- 61 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2 62 | OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r 63 | 1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G 64 | -----END PRIVATE KEY-----""" 65 | ec256PubKey = """-----BEGIN PUBLIC KEY----- 66 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9 67 | q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== 68 | -----END PUBLIC KEY-----""" 69 | 70 | ec384PrivKey = """-----BEGIN EC PRIVATE KEY----- 71 | MIGkAgEBBDCAHpFQ62QnGCEvYh/pE9QmR1C9aLcDItRbslbmhen/h1tt8AyMhske 72 | enT+rAyyPhGgBwYFK4EEACKhZANiAAQLW5ZJePZzMIPAxMtZXkEWbDF0zo9f2n4+ 73 | T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw8lE5IPUWpgu553SteKigiKLU 74 | PeNpbqmYZUkWGh3MLfVzLmx85ii2vMU= 75 | -----END EC PRIVATE KEY-----""" 76 | ec384PubKey = """-----BEGIN PUBLIC KEY----- 77 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+ 78 | Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii 79 | 1D3jaW6pmGVJFhodzC31cy5sfOYotrzF 80 | -----END PUBLIC KEY-----""" 81 | ec512PrivKey = """-----BEGIN EC PRIVATE KEY----- 82 | MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiyAa7aRHFDCh2qga 83 | 9sTUGINE5jHAFnmM8xWeT/uni5I4tNqhV5Xx0pDrmCV9mbroFtfEa0XVfKuMAxxf 84 | Z6LM/yKhgYkDgYYABAGBzgdnP798FsLuWYTDDQA7c0r3BVk8NnRUSexpQUsRilPN 85 | v3SchO0lRw9Ru86x1khnVDx+duq4BiDFcvlSAcyjLACJvjvoyTLJiA+TQFdmrear 86 | jMiZNE25pT2yWP1NUndJxPcvVtfBW48kPOmvkY4WlqP5bAwCXwbsKrCgk6xbsp12 87 | ew== 88 | -----END EC PRIVATE KEY-----""" 89 | ec512PubKey = """-----BEGIN PUBLIC KEY----- 90 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ 91 | PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47 92 | 6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM 93 | Al8G7CqwoJOsW7Kddns= 94 | -----END PUBLIC KEY-----""" 95 | 96 | 97 | proc signedRSToken(alg: string): JWT = 98 | result = tokenWithAlg(alg) 99 | result.sign(rsPrivateKey) 100 | 101 | proc signedECToken(alg, key: string): JWT = 102 | result = tokenWithAlg(alg) 103 | result.sign(key) 104 | 105 | suite "Token tests": 106 | test "Load from JSON and verify": 107 | # Load a token from json 108 | var 109 | token = getToken() 110 | secret = "secret" 111 | 112 | token.sign(secret) 113 | 114 | let b64Token = $token 115 | token = b64Token.toJWT 116 | check token.verify(secret, token.header.alg) == true 117 | 118 | test "NBF Check": 119 | let 120 | now = getTime().toUnix.int + 60 121 | token = getToken(claims = %{"nbf": %now}) 122 | expect(InvalidToken): 123 | token.verifyTimeClaims 124 | 125 | test "EXP Check": 126 | let 127 | now = getTime().toUnix.int - 60 128 | token = getToken(claims = %{"exp": %now}) 129 | expect(InvalidToken): 130 | token.verifyTimeClaims 131 | 132 | test "HS Signature": 133 | # Checked with https://jwt.io/ 134 | check: 135 | $signedHSToken("HS256") == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.sBnEuqpBDTh4Q9wnxfhWKHPbbspoz-qPNxXqVSS7ZYE" 136 | $signedHSToken("HS384") == "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.e-lF0-wO2pi5y5fCOHPFLTuHqm2hR1LIX3gaCz0xI_Nvw-KPNIpkKVcbxWl2pPz8" 137 | $signedHSToken("HS512") == "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.oAFx4658Y0Bbjko7Vm-X1AUd4XRjvnuznZk8cihzDuIRSZQjXnveoKuj8PIkAWviz-5c--R1HSyM6HZuONtrLQ" 138 | 139 | test "RS Signature": 140 | # Checked with https://jwt.io/ 141 | check: 142 | $signedRSToken("RS256") == "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.O2LIRo2GPEVHQCG3nHGvvY89__LgKLPo9EYXLDzH3oQnh_hZvlk350htpqaNMowOlxYGdM77oLsdHVxzFdto9c1pCH0jBG-HXzIKm131QxsZzCyO8ovW_2i6PGeNvsiaggrkdmOKcWcyMksasJcuqIf0h_fWhiK4wdq41Ls8ujLJpQBF3XNzOPt90so7XEvkY0zDVS0N3Bi6Hz5cN101FJFyMcDnq_3QSGMWPy829vC8PT8C0WCBIs7VdK9tEwIvpDENhRRj6cxhUqLCC0ALoynZYBeMcvOWQcz-LqbWuQGvuH2HGsN9zCpbaTdkiupNX__DKG0HUijnesYn1DkY2g" 143 | $signedRSToken("RS384") == "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.OGwjm7YvGCh4gpIuFnM7K88_dEeiSAWzpR0dXzhsne1IPygnXRKoTCdmhA2a01Mj_cW6tWhufSGcuu-7vmdm5Hi8hoDe5Q92kmM44oWikKptCy_zIM_Roe30TPjXxweE_WjV1fMZaAX6UFumikrtWCTcb9rLnSjpHYFgo-buS7cBXg_nK7xgOPz-bQvv8edVWsBWPf92B9Mak-LNZla_F5EAOjXrN16ZQ1y4qE94ro051kryqUddfVonmLSjCrCavttfBMugYf-SCbLp0w_QLaT9gA_bMXVzqyLnIj74Sr_JCWAxcYU5RaFmqZLEpowyp-m9XGdBwVS2118K0TooZg" 144 | $signedRSToken("RS512") == "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.dgb3ak_0nwQyLa2Ssmq3Jok-pr9QfVFnw_63YlFXTkq_V8r816VeCOzBRYvVv6ONvKGZDR_3SAqf3UJp1XkXtN-VyJ7VRoSHZ0d0-3DPArxDrIu20uvoQrbm4LqQtwbGPH-B-Z-7Bvfng-iwhOt1S717AepZsgVjQz2gOvBvzFsg_BDZ6nhU-5GOnIRkJ2amUt5N1TXbzKHkNLtMpKlq1BZbdv_xKSHgw_IHQRl9lIIQs_2_NuTgk8nQQiwtb9L1v3Y3KYpYGCBvgohWDcpyUKOv5f2EHekDpj1f_ALltd8gzWhIDgwK5VbBo8JAkLWDRfeTOS0fh0Faenfn551wqA" 145 | 146 | signedRSToken("RS256").verify(rsPublicKey, RS256) 147 | signedRSToken("RS384").verify(rsPublicKey, RS384) 148 | signedRSToken("RS512").verify(rsPublicKey, RS512) 149 | 150 | test "EC Signature": 151 | # Checked with https://jwt.io/ 152 | check: 153 | signedECToken("ES256", ec256PrivKey).header.toBase64 == "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9" 154 | signedECToken("ES256", ec256PrivKey).claims.toBase64 == "eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ" 155 | 156 | # We don't check signatures, as for ES* algorithms they are random 157 | 158 | signedECToken("ES256", ec256PrivKey).verify(ec256PubKey, ES256) 159 | signedECToken("ES384", ec384PrivKey).verify(ec384PubKey, ES384) 160 | signedECToken("ES512", ec512PrivKey).verify(ec512PubKey, ES512) 161 | 162 | test "header values": 163 | var token = toJWT(%*{ 164 | "header": { 165 | "alg": "HS256", 166 | "kid": "something", 167 | "typ": "JWT" 168 | }, 169 | "claims": { 170 | "userId": 1 171 | } 172 | }) 173 | token.sign(rsPrivateKey) 174 | let signed = $token 175 | let decoded = signed.toJWT() 176 | check decoded.header["kid"].getStr() == "something" 177 | --------------------------------------------------------------------------------